diff --git a/.cargo/config.toml b/.cargo/config.toml index 0e9fd44a90db..42a4adb55e1a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,8 +1,16 @@ [target.x86_64-pc-windows-msvc] rustflags = ["-Ctarget-feature=+crt-static"] [target.i686-pc-windows-msvc] -rustflags = ["-Ctarget-feature=+crt-static"] +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"] [target.'cfg(target_os="macos")'] rustflags = [ "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", ] +#[target.'cfg(target_os="linux")'] +# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change +# this is unlikely to help also, because the other so files still use libc dynamically +#rustflags = [ +# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" +#] +[net] +git-fetch-with-cli = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d8781bb0c6be..dbb8ed8a92e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -26,8 +26,8 @@ body: - type: input id: os attributes: - label: Operating system(s) on local side and remote side - description: What operating system(s) do you see this bug on? local side -> remote side. + label: Operating system(s) on local (controlling) side and remote (controlled) side + description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side. placeholder: | Windows 10 -> osx validations: @@ -35,8 +35,8 @@ body: - type: input id: version attributes: - label: RustDesk Version(s) on local side and remote side - description: What RustDesk version(s) do you see this bug on? local side -> remote side. + label: RustDesk Version(s) on local (controlling) side and remote (controlled) side + description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side. placeholder: | 1.1.9 -> 1.1.8 validations: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..56258e4e0a90 --- /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/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff new file mode 100644 index 000000000000..9b8ea26906e9 --- /dev/null +++ b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff @@ -0,0 +1,42 @@ +diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart +index 7e634cd2aa..c1e9acc295 100644 +--- a/packages/flutter/lib/src/material/dropdown_menu.dart ++++ b/packages/flutter/lib/src/material/dropdown_menu.dart +@@ -475,7 +475,7 @@ class _DropdownMenuState extends State> { + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); +- late bool _enableFilter; ++ bool _enableFilter = false; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; +@@ -524,6 +524,11 @@ class _DropdownMenuState extends State> { + } + _localTextEditingController = widget.controller ?? TextEditingController(); + } ++ if (oldWidget.enableFilter != widget.enableFilter) { ++ if (!widget.enableFilter) { ++ _enableFilter = false; ++ } ++ } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + currentHighlight = null; +@@ -663,6 +668,7 @@ class _DropdownMenuState extends State> { + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); ++ _enableFilter = false; + } + : null, + requestFocusOnHover: false, +@@ -735,6 +741,8 @@ class _DropdownMenuState extends State> { + if (_enableFilter) { + filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text) + ?? filter(widget.dropdownMenuEntries, _localTextEditingController!); ++ } else { ++ filteredEntries = widget.dropdownMenuEntries; + } + + if (widget.enableSearch) { diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index fbaf459a4d81..2d5affeef520 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,8 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.16.9" + 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: | @@ -73,12 +76,14 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd + 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 - name: Run flutter rust bridge run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h - name: Upload Artifact uses: actions/upload-artifact@master @@ -89,3 +94,5 @@ jobs: ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90f312968e07..690e3cf44fb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,9 @@ 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 + # vcpkg version: 2024.11.16 # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" + VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" on: workflow_dispatch: @@ -45,6 +45,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 @@ -92,6 +94,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install prerequisites shell: bash diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 2669f06d7a4a..a795bfaeda05 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -18,32 +18,33 @@ on: # in this file! env: - WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + 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" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.19.6" - ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). - FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" # for arm64 linux because official Dart SDK does not work 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.2.7" - NDK_VERSION: "r26d" + # vcpkg version: 2025.01.13 + VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "1.3.7" + 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 }}" jobs: + generate-bridge: + uses: ./.github/workflows/bridge.yml + build-RustDeskTempTopMostWindow: uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: @@ -56,8 +57,8 @@ jobs: fail-fast: false build-for-windows-flutter: - name: ${{ matrix.job.target }} - needs: [build-RustDeskTempTopMostWindow] + name: ${{ matrix.job.target }} + needs: [build-RustDeskTempTopMostWindow, generate-bridge] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -65,7 +66,12 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } - - { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } steps: - name: Export GitHub Actions cache environment variables @@ -77,6 +83,14 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ - name: Install LLVM and Clang uses: KyleMayes/install-llvm-action@v1 @@ -88,12 +102,27 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true + + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Patch flutter + shell: bash + run: | + cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.WIN_RUST_VERSION }} + toolchain: ${{ env.SCITER_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -101,24 +130,31 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - name: Install flutter rust bridge deps - run: | - git config --global core.longpaths true - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - Push-Location flutter ; flutter pub get ; Pop-Location - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: C:\vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies env: VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + 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:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Build rustdesk @@ -158,7 +194,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' uses: actions/upload-artifact@master with: - name: rustdesk-unsigned-windows-${{ matrix.job.arch }} + name: rustdesk-unsigned-windows-${{ matrix.job.arch }} path: rustdesk - name: Sign rustdesk files @@ -221,7 +257,12 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } - - { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static } + - { + target: i686-pc-windows-msvc, + os: windows-2022, + arch: x86, + vcpkg-triplet: x86-windows-static, + } # - { target: aarch64-pc-windows-msvc, os: windows-2022 } steps: - name: Export GitHub Actions cache environment variables @@ -232,7 +273,9 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Install LLVM and Clang uses: rustdesk-org/install-llvm-action-32bit@master @@ -255,12 +298,26 @@ jobs: with: vcpkgDirectory: C:\vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies env: VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + 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:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Build rustdesk @@ -280,7 +337,7 @@ jobs: # Do not remove x64 files, because the user may run the 32bit version on a 64bit system. # Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64, # we need the x64 exe to install and uninstall the driver. - rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat + rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat mv ./usbmmidd_v2 ./Release || true - name: find Runner.res @@ -336,6 +393,7 @@ jobs: # 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 @@ -345,14 +403,15 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ - name: Build rustdesk run: | @@ -408,6 +467,7 @@ jobs: if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -430,21 +490,43 @@ jobs: run: | brew install nasm yasm - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 with: vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + 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:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Install Rust toolchain @@ -459,17 +541,22 @@ jobs: prefix-key: rustdesk-lib-cache-ios key: ${{ matrix.job.target }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ - name: Build rustdesk lib run: | rustup target add ${{ matrix.job.target }} cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Upload liblibrustdesk.a Artifacts + uses: actions/upload-artifact@master + with: + name: liblibrustdesk.a + path: target/aarch64-apple-ios/release/liblibrustdesk.a - name: Build rustdesk shell: bash @@ -499,6 +586,7 @@ jobs: #if: ${{ inputs.upload-artifact }} if: false runs-on: [self-hosted, macOS, ARM64] + needs: [generate-bridge] strategy: fail-fast: false steps: @@ -510,16 +598,17 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ - name: Build rustdesk lib run: | @@ -554,6 +643,7 @@ jobs: build-for-macOS: name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -563,13 +653,15 @@ jobs: os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel extra-build-args: "", arch: x86_64, + vcpkg-triplet: x64-osx, } - { target: aarch64-apple-darwin, os: macos-latest, # 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: "", + extra-build-args: "--screencapturekit", arch: aarch64, + vcpkg-triplet: arm64-osx, } steps: - name: Export GitHub Actions cache environment variables @@ -580,7 +672,9 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null @@ -617,7 +711,13 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config + brew install llvm create-dmg nasm cmake gcc wget ninja + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi - name: Install flutter uses: subosito/flutter-action@v2 @@ -625,6 +725,11 @@ jobs: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - name: Workaround for flutter issue shell: bash run: | @@ -636,7 +741,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VERSION }} + toolchain: ${{ env.MAC_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -644,21 +749,33 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 with: vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | - $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true - name: Show version information (Rust, cargo, Clang) shell: bash @@ -672,6 +789,13 @@ jobs: - name: Build rustdesk run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + 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 }} - name: create unsigned dmg @@ -759,11 +883,8 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz - generate-bridge-linux: - uses: ./.github/workflows/bridge.yml - build-rustdesk-android: - needs: [generate-bridge-linux] + needs: [generate-bridge] name: build rustdesk android apk ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} strategy: @@ -774,15 +895,35 @@ jobs: arch: aarch64, target: aarch64-linux-android, os: ubuntu-20.04, - openssl-arch: android-arm64, + reltype: release, + suffix: "", } - { arch: armv7, target: armv7-linux-androideabi, os: ubuntu-20.04, - openssl-arch: android-arm, + reltype: release, + suffix: "", + } + - { + arch: x86_64, + target: x86_64-linux-android, + os: ubuntu-20.04, + reltype: release, + suffix: "", } steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 with: @@ -801,7 +942,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -811,7 +952,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -820,18 +960,27 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-11-jdk-headless \ + openjdk-17-jdk-headless \ pkg-config \ tree \ wget - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - uses: nttld/setup-ndk@v1 id: setup-ndk with: @@ -843,17 +992,34 @@ jobs: with: vcpkgDirectory: /opt/artifacts/vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | case ${{ matrix.job.target }} in aarch64-linux-android) - ./flutter/build_android_deps.sh arm64-v8a + ANDROID_TARGET=arm64-v8a ;; armv7-linux-androideabi) - ./flutter/build_android_deps.sh armeabi-v7a + ANDROID_TARGET=armeabi-v7a + ;; + x86_64-linux-android) + ANDROID_TARGET=x86_64 + ;; + i686-linux-android) + ANDROID_TARGET=x86 ;; esac + if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Restore bridge files @@ -873,20 +1039,13 @@ 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: | - sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml - cd flutter/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 }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} run: | rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh @@ -898,14 +1057,30 @@ jobs: mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so ;; + x86_64-linux-android) + ./flutter/ndk_x64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + ;; + i686-linux-android) + ./flutter/ndk_x86.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + ;; esac + - name: Upload Rustdesk library to Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk.so.${{ matrix.job.target }} + path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so + - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH # temporary use debug sign config sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle case ${{ matrix.job.target }} in @@ -915,8 +1090,8 @@ jobs: cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so # build flutter pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ;; armv7-linux-androideabi) mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a @@ -924,13 +1099,39 @@ jobs: cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so # build flutter pushd flutter - flutter build apk --release --target-platform android-arm --split-per-abi - mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + x86_64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + i686-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ;; esac popd mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk . + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk . + + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION - uses: r0adkll/sign-android-release@v1 name: Sign app APK @@ -943,8 +1144,8 @@ jobs: keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} env: - # override default build-tools version (29.0.3) -- optional - BUILD_TOOLS_VERSION: "30.0.2" + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} - name: Upload Artifacts if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' @@ -971,9 +1172,190 @@ jobs: files: | signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + build-rustdesk-android-universal: + needs: [build-rustdesk-android] + name: build rustdesk android universal apk + if: ${{ inputs.upload-artifact }} + runs-on: ubuntu-20.04 + env: + reltype: release + x86_target: "" # can be ",android-x86" + suffix: "" + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + - 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: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libc6-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + openjdk-17-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.aarch64-linux-android + path: ./flutter/android/app/src/main/jniLibs/arm64-v8a + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.armv7-linux-androideabi + path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.x86_64-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86_64 + + - name: Download Rustdesk library from Artifacts + if: ${{ env.reltype == 'debug' }} + uses: actions/download-artifact@master + with: + name: librustdesk.so.i686-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86 + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + if [ "${{ env.reltype }}" = "debug" ]; then + mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + fi + # build flutter + pushd flutter + flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }} + popd + mkdir -p signed-apk + mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk + + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} + + - 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: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish signed apk 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: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish unsigned apk 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: | + signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk + build-rustdesk-linux: - needs: [generate-bridge-linux] - name: build rustdesk linux ${{ matrix.job.target }} + needs: [generate-bridge] + name: build rustdesk linux ${{ matrix.job.target }} runs-on: ${{ matrix.job.on }} strategy: fail-fast: false @@ -992,7 +1374,7 @@ jobs: arch: aarch64, target: aarch64-unknown-linux-gnu, distro: ubuntu18.04, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, deb_arch: arm64, vcpkg-triplet: arm64-linux, } @@ -1005,16 +1387,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@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Set Swap Space if: ${{ matrix.job.arch == 'x86_64' }} @@ -1058,11 +1444,26 @@ jobs: with: vcpkgDirectory: /opt/artifacts/vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + sudo apt install -y libva-dev && apt show libva-dev + 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:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Restore bridge files @@ -1098,7 +1499,7 @@ jobs: gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-10-dev \ libgstreamer1.0-dev \ @@ -1107,7 +1508,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1194,6 +1594,19 @@ jobs: ;; esac + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + case ${{ matrix.job.arch }} in + aarch64) + pushd /opt/flutter-elinux/flutter + ;; + x86_64) + pushd /opt/flutter + ;; + esac + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + # build flutter pushd /workspace export CARGO_INCREMENTAL=0 @@ -1241,16 +1654,16 @@ jobs: rustdesk-*.deb rustdesk-*.rpm - - name: Upload deb + - name: Upload deb uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb - + # only x86_64 for arch since we can not find newest arm64 docker image to build # old arch image does not make sense for arch since it is "arch" which always update to date - # and failed to makepkg arm64 on x86_64 + # and failed to makepkg arm64 on x86_64 - name: Patch archlinux PKGBUILD if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' run: | @@ -1278,7 +1691,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: @@ -1299,7 +1711,7 @@ jobs: - { arch: armv7, target: armv7-unknown-linux-gnueabihf, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, distro: ubuntu18.04-rustdesk, deb_arch: armhf, sciter_arch: arm32, @@ -1315,7 +1727,9 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Free Space run: | @@ -1325,7 +1739,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VERSION }} + toolchain: ${{ env.SCITER_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -1345,19 +1759,17 @@ jobs: ls -l "${PWD}" dockerRunArgs: | --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" shell: /bin/bash install: | apt-get update apt-get install -y \ build-essential \ clang \ - cmake \ curl \ gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-dev \ libdbus-1-dev \ @@ -1369,7 +1781,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1384,49 +1795,87 @@ jobs: wget \ xz-utils \ zip - # install newer nasm for aom - wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_2.14-1_${{ matrix.job.deb_arch }}.deb" - dpkg -i nasm.deb - rm -f nasm.deb - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Set python3.7 as default python3 - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 - # vcpkg + # arm-linux needs CMake and vcokg built from source as there + # are no prebuilts available from Kitware and Microsoft + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + # install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake + apt-get install -y gcc-8 g++-8 libssl-dev + # bootstrap CMake amd add it to PATH + git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake + pushd /tmp/cmake + ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" + make -j1 install + popd + rm -rf /tmp/cmake + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + fi + # bootstrap vcpkg and set VCPKG_ROOT export VCPKG_ROOT=/opt/artifacts/vcpkg + mkdir -p /opt/artifacts pushd /opt/artifacts rm -rf vcpkg git clone https://github.com/microsoft/vcpkg pushd vcpkg git reset --hard ${{ env.VCPKG_COMMIT_ID }} - sh bootstrap-vcpkg.sh -disableMetrics + # build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics + else + sh bootstrap-vcpkg.sh -disableMetrics + fi popd popd - # override Linux compiler detection in vcpkg - cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" # rust pushd /opt # do not use rustup, because memory overflow in qemu - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh + pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + ./install.sh + popd rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} - # edit config + popd + # install newer nasm for aom + wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb" + dpkg -i nasm.deb + rm -f nasm.deb + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # set python3.7 as default python3 + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 + # add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + export VCPKG_FORCE_SYSTEM_BINARIES=1 + fi + # edit cargo config mkdir -p ~/.cargo/ echo """ [source.crates-io] registry = 'https://github.com/rust-lang/crates.io-index' """ > ~/.cargo/config cat ~/.cargo/config - - # build - pushd /workspace + # 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:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + # build rustdesk python3 ./res/inline-sciter.py export CARGO_INCREMENTAL=0 cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1 - # package + # make debian package mkdir -p ./Release mv ./target/release/rustdesk ./Release/rustdesk wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so @@ -1450,7 +1899,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb - - name: Upload deb + - name: Upload deb uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' with: @@ -1466,17 +1915,13 @@ jobs: fail-fast: false matrix: job: - - { - target: x86_64-unknown-linux-gnu, - arch: x86_64, - } - - { - target: aarch64-unknown-linux-gnu, - arch: aarch64, - } + - { target: x86_64-unknown-linux-gnu, arch: x86_64 } + - { target: aarch64-unknown-linux-gnu, arch: aarch64 } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -1515,7 +1960,7 @@ jobs: build-flatpak: name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} - needs: + needs: - build-rustdesk-linux - build-rustdesk-linux-sciter runs-on: ${{ matrix.job.on }} @@ -1542,13 +1987,15 @@ jobs: target: aarch64-unknown-linux-gnu, # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" distro: ubuntu22.04, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, arch: aarch64, suffix: "", } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -1574,36 +2021,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 @@ -1624,7 +2052,9 @@ jobs: RELEASE_NAME: web-basic steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Prepare env run: | @@ -1636,7 +2066,12 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true + + - name: Patch flutter + shell: bash + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff # https://rustdesk.com/docs/en/dev/build/web/ - name: Build web diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index d788858ab4c5..7cb1f801c529 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -16,9 +16,9 @@ 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.2.7" + # vcpkg version: 2024.11.16 + VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" + VERSION: "1.3.7" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -90,7 +90,8 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} - + submodules: recursive + - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 @@ -149,7 +150,7 @@ jobs: shell: bash run: | sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml; - cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" + cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked # below works for mac to make buildable on 3.13.9 # pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd; pushd flutter && flutter pub get && popd @@ -250,6 +251,7 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} + submodules: recursive - name: Install dependencies run: | @@ -262,7 +264,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev\ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -302,7 +304,7 @@ jobs: - name: Install flutter rust bridge deps run: | git config --global core.longpaths true - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd; pushd flutter ; flutter pub get ; popd @@ -347,7 +349,7 @@ jobs: ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} run: | rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh diff --git a/.gitignore b/.gitignore index 30e1aafe2f10..b4ea62660468 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ examples/**/target/ vcpkg_installed flutter/lib/generated_plugin_registrant.dart libsciter.dylib +flutter/web/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..d80e69aa84a4 --- /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/Cargo.lock b/Cargo.lock index 936d12b01325..4b762259505d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android-wakelock" version = "0.1.0" -source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" +source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" dependencies = [ "jni 0.21.1", "log", @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6" +source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,24 +234,13 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", - "resvg", + "serde 1.0.203", + "serde_derive", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "async-broadcast" version = "0.5.1" @@ -734,9 +723,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -746,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde 1.0.203", ] @@ -871,6 +860,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -898,6 +893,20 @@ dependencies = [ "regex", ] +[[package]] +name = "cidre" +version = "0.4.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" +dependencies = [ + "cidre-macros", + "parking_lot", +] + +[[package]] +name = "cidre-macros" +version = "0.1.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" + [[package]] name = "cipher" version = "0.4.4" @@ -987,11 +996,14 @@ dependencies = [ [[package]] name = "clipboard-master" version = "4.0.0-beta.6" -source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c" +source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695" dependencies = [ "objc", "objc-foundation", "objc_id", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", "windows-win", "wl-clipboard-rs", "x11-clipboard 0.9.2", @@ -1000,9 +1012,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1278,10 +1290,10 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a" dependencies = [ "alsa", + "cidre", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "coreaudio-rs", "dasp_sample", @@ -1526,12 +1538,6 @@ dependencies = [ "dasp_sample", ] -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "dbus" version = "0.9.7" @@ -1575,6 +1581,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" @@ -2081,12 +2097,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -2143,29 +2153,6 @@ dependencies = [ "libm", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" -dependencies = [ - "roxmltree 0.19.0", -] - -[[package]] -name = "fontdb" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - [[package]] name = "foreign-types" version = "0.3.2" @@ -2274,9 +2261,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2284,9 +2271,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -2301,9 +2288,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -2335,9 +2322,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -2346,21 +2333,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -2924,6 +2911,7 @@ dependencies = [ "bytes", "chrono", "confy", + "default_net", "directories-next", "dirs-next", "dlopen", @@ -2948,6 +2936,7 @@ dependencies = [ "serde 1.0.203", "serde_derive", "serde_json 1.0.118", + "sha2", "socket2 0.3.19", "sodiumoxide", "sysinfo", @@ -3087,8 +3076,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.5.1" -source = "git+https://github.com/21pages/hwcodec#74e8288f776a9d43861f16aa62e86b57c7209868" +version = "0.7.1" +source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9" dependencies = [ "bindgen 0.59.2", "cc", @@ -3213,16 +3202,10 @@ dependencies = [ "tiff", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "impersonate_system" version = "0.1.0" -source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" +source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" dependencies = [ "cc", ] @@ -3462,16 +3445,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kurbo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" -dependencies = [ - "arrayvec", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3733,7 +3706,7 @@ dependencies = [ [[package]] name = "machine-uid" version = "0.3.0" -source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" +source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" dependencies = [ "bindgen 0.59.2", "cc", @@ -3777,15 +3750,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.6.5" @@ -3847,14 +3811,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mouce" -version = "0.2.1" -source = "git+https://github.com/rustdesk-org/mouce.git#177625a395cd8fa73964714d0039535cb9b47893" -dependencies = [ - "glob", -] - [[package]] name = "muda" version = "0.13.5" @@ -4043,11 +3999,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -4207,7 +4175,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.86", "quote 1.0.36", "syn 2.0.68", @@ -4426,9 +4394,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", @@ -4458,9 +4426,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -4732,15 +4700,9 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher 0.2.3", + "siphasher", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.5" @@ -5065,6 +5027,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "0.6.13" @@ -5260,7 +5231,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" +source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", @@ -5414,31 +5385,6 @@ dependencies = [ "winreg 0.50.0", ] -[[package]] -name = "resvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" -dependencies = [ - "gif", - "jpeg-decoder", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.17.8" @@ -5463,18 +5409,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "roxmltree" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rpassword" version = "2.1.0" @@ -5544,7 +5478,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", ] @@ -5572,7 +5506,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.2.7" +version = "1.3.7" dependencies = [ "android-wakelock", "android_logger", @@ -5604,6 +5538,7 @@ dependencies = [ "flutter_rust_bridge", "fon", "fruitbasket", + "gtk", "hbb_common", "hex", "hound", @@ -5618,7 +5553,7 @@ dependencies = [ "libpulse-simple-binding", "mac_address", "magnum-opus", - "mouce", + "nix 0.29.0", "num_cpus", "objc", "objc_id", @@ -5650,6 +5585,7 @@ dependencies = [ "system_shutdown", "tao", "tauri-winrt-notification", + "termios", "totp-rs", "tray-icon", "url", @@ -5670,7 +5606,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.7" dependencies = [ "brotli", "dirs 5.0.1", @@ -5853,22 +5789,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rustybuzz" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" -dependencies = [ - "bitflags 2.6.0", - "bytemuck", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.18" @@ -5905,7 +5825,7 @@ dependencies = [ [[package]] name = "sciter-rs" version = "0.5.57" -source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#fab913b7c2e779b05c249b0c5de5a08759b2c15d" +source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" dependencies = [ "lazy_static", "libc", @@ -6159,27 +6079,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - [[package]] name = "siphasher" 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" @@ -6189,15 +6094,6 @@ dependencies = [ "autocfg 1.3.0", ] -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -6268,15 +6164,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - [[package]] name = "strsim" version = "0.8.0" @@ -6338,16 +6225,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svgtypes" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" -dependencies = [ - "kurbo", - "siphasher 1.0.1", -] - [[package]] name = "syn" version = "0.15.44" @@ -6399,7 +6276,7 @@ dependencies = [ [[package]] name = "sysinfo" version = "0.29.10" -source = "git+https://github.com/rustdesk-org/sysinfo#f45dcc6510d48c3a1401c5a33eedccc8899f67b2" +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)", @@ -6593,11 +6470,11 @@ dependencies = [ [[package]] name = "tfc" -version = "0.6.1" -source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0" dependencies = [ "anyhow", - "core-graphics 0.22.3", + "core-graphics 0.23.2", "unicode-segmentation", "winapi 0.3.9", "x11 2.19.0", @@ -6687,32 +6564,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if 1.0.0", - "log", - "png", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinyvec" version = "1.6.1" @@ -7004,12 +6855,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "typenum" version = "1.17.0" @@ -7082,18 +6927,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" - -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -7109,30 +6942,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-script" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" - [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.1.13" @@ -7195,33 +7010,6 @@ dependencies = [ "log", ] -[[package]] -name = "usvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.20.0", - "rustybuzz", - "simplecss", - "siphasher 1.0.1", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf16string" version = "0.2.0" @@ -7309,7 +7097,7 @@ dependencies = [ [[package]] name = "wallpaper" version = "3.2.0" -source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" +source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" dependencies = [ "dirs 5.0.1", "enquote", @@ -7414,9 +7202,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -7428,9 +7216,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ "bitflags 2.6.0", "rustix 0.38.34", @@ -7440,9 +7228,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7452,9 +7240,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7465,20 +7253,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2 1.0.86", - "quick-xml 0.31.0", + "quick-xml 0.34.0", "quote 1.0.36", ] [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -7498,7 +7286,7 @@ dependencies = [ [[package]] name = "webm" version = "1.1.0" -source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "webm-sys", ] @@ -7506,7 +7294,7 @@ dependencies = [ [[package]] name = "webm-sys" version = "1.0.4" -source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "cc", ] @@ -8220,12 +8008,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zbus" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 85b2b540121d..917b49a879c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.2.7" +version = "1.3.7" 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 = [] @@ -36,6 +40,7 @@ unix-file-copy-paste = [ "dep:once_cell", "clipboard/unix-file-copy-paste", ] +screencapturekit = ["cpal/screencapturekit"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -78,19 +83,20 @@ zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } -[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] -cpal = "0.15" +[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 = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] } +arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } system_shutdown = "4.0" @@ -114,7 +120,7 @@ winapi = { version = "0.3", features = [ winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display" } -impersonate_system = { git = "https://github.com/21pages/impersonate-system" } +impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } shared_memory = "0.12" tauri-winrt-notification = "0.1.2" runas = "1.2" @@ -138,7 +144,7 @@ image = "0.24" keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] -wallpaper = { git = "https://github.com/21pages/wallpaper.rs" } +wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" } [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support @@ -150,9 +156,8 @@ 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" -mouce = { git="https://github.com/rustdesk-org/mouce.git" } evdev = { git="https://github.com/rustdesk-org/evdev" } dbus = "0.9" dbus-crossroads = "0.5" @@ -162,11 +167,14 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/ x11rb = {version = "0.12", features = ["all-extensions"], optional = true} percent-encoding = {version = "2.3", optional = true} once_cell = {version = "1.18", optional = true} +nix = { version = "0.29", features = ["term", "process"]} +gtk = "0.18" +termios = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" jni = "0.21" -android-wakelock = { git = "https://github.com/21pages/android-wakelock" } +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"] diff --git a/Dockerfile b/Dockerfile index 8544219c2b1c..f0e4e4a4a622 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 df8c1ca3483a..1c4d6be4afe9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@

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

@@ -25,9 +24,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 @@ -59,19 +61,19 @@ Please download Sciter dynamic library yourself. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + 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 +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) @@ -171,3 +173,4 @@ Please ensure that you are running these commands from the root of the RustDesk ![File Transfer](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/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 411d7bb571d9..a5ae4ef78c9d 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,8 +18,8 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.2.7 - exec: usr/lib/rustdesk/rustdesk + version: 1.3.7 + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -47,9 +47,9 @@ AppDir: - libasound2 - libsystemd0 - curl + - libva2 - libva-drm2 - libva-x11-2 - - libvdpau1 - libgstreamer-plugins-base1.0-0 - gstreamer1.0-pipewire - libwayland-client0 @@ -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: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9c6860dd43e6..25889d5b7d0c 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.2.7 - exec: usr/lib/rustdesk/rustdesk + version: 1.3.7 + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -37,6 +37,9 @@ AppDir: - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted universe multiverse include: + # https://github.com/rustdesk/rustdesk/issues/9103 + # Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try. + # We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it. - libc6:amd64 - libgtk-3-0 - libxcb-randr0 @@ -47,9 +50,9 @@ AppDir: - libasound2 - libsystemd0 - curl + - libva2 - libva-drm2 - libva-x11-2 - - libvdpau1 - libgstreamer-plugins-base1.0-0 - gstreamer1.0-pipewire - libwayland-client0 @@ -77,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: diff --git a/build.py b/build.py index 97ec9dfe9f9e..87c0dbd3432d 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( @@ -31,6 +32,11 @@ def get_deb_arch() -> str: return "amd64" return custom_arch +def get_deb_extra_depends() -> str: + custom_arch = os.environ.get("DEB_ARCH") + if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so + return ", libatomic1" + return "" def system2(cmd): exit_code = os.system(cmd) @@ -106,7 +112,7 @@ def make_parser(): '--hwcodec', action='store_true', help='Enable feature hwcodec' + ( - '' if windows or osx else ', need libva-dev, libvdpau-dev.') + '' if windows or osx else ', need libva-dev.') ) parser.add_argument( '--vram', @@ -138,6 +144,12 @@ def make_parser(): "--package", type=str ) + if osx: + parser.add_argument( + '--screencapturekit', + action='store_true', + help='Enable feature screencapturekit' + ) return parser @@ -269,6 +281,9 @@ def get_features(args): features.append('flutter') if args.unix_file_copy_paste: features.append('unix-file-copy-paste') + if osx: + if args.screencapturekit: + features.append('screencapturekit') print("features:", features) return features @@ -278,14 +293,17 @@ def generate_control_file(version): system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk +Section: net +Priority: optional Version: %s Architecture: %s Maintainer: rustdesk Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire +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. -""" % (version, get_deb_arch()) +""" % (version, get_deb_arch(), get_deb_extra_depends()) file = open(control_file_path, "w") file.write(content) file.close() @@ -304,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/') @@ -314,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( @@ -325,8 +343,6 @@ def build_flutter_deb(version, features): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2( - 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') system2( 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') system2( @@ -339,7 +355,7 @@ 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/') @@ -351,7 +367,7 @@ def build_flutter_deb(version, features): 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/') @@ -359,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( @@ -370,15 +386,13 @@ def build_deb_from_folder(version, binary_folder): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2( - 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') system2( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") 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/') @@ -391,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") @@ -608,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/build.rs b/build.rs index d332a43a22eb..3d19ee037f6f 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ #[cfg(windows)] fn build_windows() { let file = "src/platform/windows.cc"; - let file2 = "src/platform/windows_delete_test_cert.cc"; + let file2 = "src/platform/windows_delete_test_cert.cc"; cc::Build::new().file(file).file(file2).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); println!("cargo:rerun-if-changed={}", file); @@ -61,7 +61,11 @@ fn install_android_deps() { let target = format!("{}-android", target_arch); let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); let mut path: std::path::PathBuf = vcpkg_root.into(); - path.push("installed"); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } path.push(target); println!( "{}", @@ -72,7 +76,6 @@ fn install_android_deps() { ); println!("cargo:rustc-link-lib=ndk_compat"); println!("cargo:rustc-link-lib=oboe"); - println!("cargo:rustc-link-lib=oboe_wrapper"); println!("cargo:rustc-link-lib=c++"); println!("cargo:rustc-link-lib=OpenSLES"); } diff --git a/docs/CODE_OF_CONDUCT-NO.md b/docs/CODE_OF_CONDUCT-NO.md new file mode 100644 index 000000000000..baefda0519ac --- /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/CODE_OF_CONDUCT-ZH.md b/docs/CODE_OF_CONDUCT-ZH.md new file mode 100644 index 000000000000..0877ab20f246 --- /dev/null +++ b/docs/CODE_OF_CONDUCT-ZH.md @@ -0,0 +1,87 @@ + +# 贡献者公约行为准则 + +## 我们的承诺 + +身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。 + +我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。 + +## 我们的标准 + +有助于为我们的社区创造积极环境的行为例子包括但不限于: + +* 表现出对他人的同情和善意 +* 尊重不同的主张、观点和感受 +* 提出和大方接受建设性意见 +* 承担责任并向受我们错误影响的人道歉 +* 注重社区共同诉求,而非个人得失 + +不当行为例子包括: + +* 使用情色化的语言或图像,及性引诱或挑逗 +* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击 +* 公开或私下的骚扰行为 +* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址 +* 其他有理由认定为违反职业操守的不当行为 + +## 责任和权力 + +社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。 + +社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。 + +## 适用范围 + +本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。 + +代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。 + +## 监督 + +辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。 + +所有社区领袖都有义务尊重任何事件报告者的隐私和安全。 + +## 处理方针 + +社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式: + +### 1. 纠正 + +**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。 + +**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。 + +### 2. 警告 + +**社区影响**: 单个或一系列违规行为。 + +**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。 + +### 3. 临时封禁 + +**社区影响**: 严重违反社区准则,包括持续的不当行为。 + +**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。 + +### 4. 永久封禁 + +**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。 + +**处理意见**: 永久禁止在社区内进行任何形式的公开互动。 + +## 参见 + +本行为准则改编自[参与者公约][homepage]2.0 版, 参见 +[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]. + +指导方针借鉴自[Mozilla纪检分级][Mozilla CoC]. + +有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。 + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/zh-cn/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 \ No newline at end of file diff --git a/docs/CONTRIBUTING-NO.md b/docs/CONTRIBUTING-NO.md new file mode 100644 index 000000000000..89a574563275 --- /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-ZH.md b/docs/CONTRIBUTING-ZH.md new file mode 100644 index 000000000000..718cdac69bb5 --- /dev/null +++ b/docs/CONTRIBUTING-ZH.md @@ -0,0 +1,32 @@ +# 为RustDesk做贡献 + +Rust欢迎每一位贡献者,如果您有意向为我们做出贡献,请遵循以下指南: + +## 贡献方式 + +对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。 + +如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。 + +## PR 注意事项 + +- 从 master 分支创建一个新的分支,并在提交PR之前,如果需要,将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。 + +- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。 + +- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名 + +- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。 + +- 请为修复的 bug 或新增的功能添加相应的测试用例。 + +有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## 行为准则 + +请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。 + + +## 沟通渠道 + +RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。 diff --git a/docs/DEVCONTAINER-NO.md b/docs/DEVCONTAINER-NO.md new file mode 100644 index 000000000000..1d944ed5d6c6 --- /dev/null +++ b/docs/DEVCONTAINER-NO.md @@ -0,0 +1,14 @@ + +Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget. + +Nå tilbyr devcontainer linux og android builds i både debug og release modus. + +Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds. + +Kommando|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|release + diff --git a/docs/README-KR.md b/docs/README-KR.md index af3c665244f3..b0a8b973e2ad 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -9,12 +9,12 @@ README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.

-Chat with us: [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) [![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 서버를 사용해도, [직접 설정](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) @@ -43,9 +43,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C - 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 -- run `cargo run` +- 실행 `cargo run` -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [빌드](https://rustdesk.com/docs/en/dev/build/) ## Linux에서 빌드 순서 @@ -67,7 +67,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` -### Install vcpkg +### vcpkg 설치 ```sh git clone https://github.com/microsoft/vcpkg @@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus aom ``` -### Fix libvpx (For Fedora) +### libvpx 수정 (For Fedora용) ```sh cd vcpkg/buildtrees/libvpx/src @@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### 빌드 ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ## Docker에 빌드하는 방법 -레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. +리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. ```sh git clone https://github.com/rustdesk/rustdesk @@ -115,13 +115,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다. +이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다. ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 커맨드에 다른 인수를 지정할 필요가 있다면, 커맨드 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 커맨드 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 커맨드로 실행할 수 있습니다. +첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다. ```sh target/debug/rustdesk @@ -133,9 +133,9 @@ target/debug/rustdesk target/release/rustdesk ``` -커맨드를 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 서브커맨드는 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다. +명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다. -## File Structure +## 파일 구조 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, tcp/udp 랩퍼, protobuf, 파일 전송을 위한 fs 함수, 그 외 유틸리티 함수 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처 @@ -143,12 +143,12 @@ target/release/rustdesk - **[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/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 code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드 +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트 -## Snapshot +## 스냅샷 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-NO.md b/docs/README-NO.md new file mode 100644 index 000000000000..dbe4ddd2deb5 --- /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) + +[![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](docs/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 4d3464d41ae6..ef8f42648c05 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -164,3 +164,4 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru ![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) + diff --git a/docs/README-UA.md b/docs/README-UA.md index c4d2e6f9f390..98f19d4e69d7 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -1,20 +1,18 @@

RustDesk - Ваша віддалена стільниця
- Сервери • + СервериЗбиранняDockerСтруктура • - Знімки
- [English] | [č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 на вашу рідну мову + Знімки екрана
+ [English] | [č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 вашою рідною мовою

Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open) - Ще один застосунок для віддаленого керування стільницею, написаний на 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) @@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT ```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 + 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 +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) @@ -158,18 +156,19 @@ target/release/rustdesk - **[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)**: графічний інтерфейс користувача - **[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/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter + +## Знімки екрана -## Знімки +![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 54b9c29a1417..4920ade6d960 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -8,7 +8,7 @@ [English] | [Українська] | [česky] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

-Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) +与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). +RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md). [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m ## 依赖 -桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。 +桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。 + +请自行下载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) | @@ -133,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器 ``` 在Dockerfile的RUN apt update之前插入两行: - RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list - RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \ + sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list ``` 2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: @@ -207,12 +209,13 @@ target/release/rustdesk - **[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)**: GUI +- **[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)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 ## 截图 diff --git a/docs/SECURITY-NO.md b/docs/SECURITY-NO.md new file mode 100644 index 000000000000..1f8dcb411bdc --- /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/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml new file mode 100644 index 000000000000..0d3b33bb8c3d --- /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 6d7acb5b89ca..af1bc5fe74a1 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 d7f41bf0ec09..000000000000 --- 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/build.gradle b/flutter/android/app/build.gradle index 320eb3347c0e..c554251656a8 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -1,6 +1,9 @@ import com.google.protobuf.gradle.* plugins { id "com.google.protobuf" version "0.9.4" + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() @@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -32,10 +30,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - dependencies { implementation 'com.google.protobuf:protobuf-javalite:3.20.1' } @@ -57,7 +51,7 @@ protobuf { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -105,7 +99,6 @@ flutter { dependencies { implementation "androidx.media:media:1.6.0" implementation 'com.github.getActivity:XXPermissions:18.5' - implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } + implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } } implementation 'com.caverock:androidsvg-aar:1.4' } - diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 51015f74a831..47533612b3d5 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,13 @@ + + + + + + + when (menuItem.itemId) { @@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { openMainActivity() true } + idSyncClipboard -> { + syncClipboard() + true + } idStopService -> { stopMainService() true @@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { } } + private fun syncClipboard() { + MainActivity.rdClipboardManager?.syncClipboard(false) + } + private fun stopMainService() { MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) } 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 3fcc72df346d..f53f95d6754e 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 @@ -280,20 +280,20 @@ class InputService : AccessibilityService() { var textToCommit: String? = null - if (keyboardMode == KeyboardMode.Legacy) { - if (keyEvent.hasChr() && keyEvent.getDown()) { + // [down] indicates the key's state(down or up). + // [press] indicates a click event(down and up). + // https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688 + if (keyEvent.hasSeq()) { + textToCommit = keyEvent.getSeq() + } else if (keyboardMode == KeyboardMode.Legacy) { + if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) { val chr = keyEvent.getChr() if (chr != null) { textToCommit = String(Character.toChars(chr)) } } } else if (keyboardMode == KeyboardMode.Translate) { - if (keyEvent.hasSeq() && keyEvent.getDown()) { - val seq = keyEvent.getSeq() - if (seq != null) { - textToCommit = seq - } - } + } else { } Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") @@ -320,6 +320,10 @@ class InputService : AccessibilityService() { } else { ke?.let { event -> inputConnection.sendKeyEvent(event) + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + inputConnection.sendKeyEvent(actionUpEvent) + } } } } @@ -333,6 +337,10 @@ class InputService : AccessibilityService() { for (item in possibleNodes) { val success = trySendKeyEvent(event, item, textToCommit) if (success) { + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + trySendKeyEvent(actionUpEvent, item, textToCommit) + } break } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt index 1e63df4054f5..ccb33195e9fb 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt @@ -31,14 +31,12 @@ object KeyEventConverter { } var action = 0 - if (keyEventProto.getDown()) { + if (keyEventProto.getDown() || keyEventProto.getPress()) { action = KeyEvent.ACTION_DOWN } else { action = KeyEvent.ACTION_UP } - // FIXME: The last parameter is the repeat count, not modifiers ? - // https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int) return KeyEvent(0, 0, action, chrValue, 0, modifiers) } 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 10c3d7c2d78f..5c54c18fb821 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 @@ -13,6 +13,8 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.ClipboardManager +import android.os.Bundle import android.os.Build import android.os.IBinder import android.util.Log @@ -36,6 +38,9 @@ import kotlin.concurrent.thread class MainActivity : FlutterActivity() { companion object { var flutterMethodChannel: MethodChannel? = null + private var _rdClipboardManager: RdClipboardManager? = null + val rdClipboardManager: RdClipboardManager? + get() = _rdClipboardManager; } private val channelTag = "mChannel" @@ -85,6 +90,14 @@ class MainActivity : FlutterActivity() { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (_rdClipboardManager == null) { + _rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) + FFI.setClipboardManager(_rdClipboardManager!!) + } + } + override fun onDestroy() { Log.e(logTag, "onDestroy") mainService?.let { @@ -207,6 +220,10 @@ class MainActivity : FlutterActivity() { result.success(true) } + "try_sync_clipboard" -> { + rdClipboardManager?.syncClipboard(true) + result.success(true) + } GET_START_ON_BOOT_OPT -> { val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) 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 1a709747e25c..e9ec0975d1be 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 @@ -302,6 +302,8 @@ class MainService : Service() { stopCapture() FFI.refreshScreen() startCapture() + } else { + FFI.refreshScreen() } } @@ -431,6 +433,7 @@ class MainService : Service() { checkMediaPermission() _isStart = true FFI.setFrameRawEnable("video",true) + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) return true } @@ -439,6 +442,7 @@ class MainService : Service() { Log.d(logTag, "Stop Capture") FFI.setFrameRawEnable("video",false) _isStart = false + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) // release video if (reuseVirtualDisplay) { // The virtual display video projection can be paused by calling `setSurface(null)`. diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt new file mode 100644 index 000000000000..8c9d85028402 --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt @@ -0,0 +1,197 @@ +package com.carriez.flutter_hbb + +import java.nio.ByteBuffer +import java.util.Timer +import java.util.TimerTask + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.util.Log +import androidx.annotation.Keep + +import hbb.MessageOuterClass.ClipboardFormat +import hbb.MessageOuterClass.Clipboard +import hbb.MessageOuterClass.MultiClipboards + +import ffi.FFI + +class RdClipboardManager(private val clipboardManager: ClipboardManager) { + private val logTag = "RdClipboardManager" + private val supportedMimeTypes = arrayOf( + ClipDescription.MIMETYPE_TEXT_PLAIN, + ClipDescription.MIMETYPE_TEXT_HTML + ) + + // 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`. + // 2. Avoid sending the clipboard data before enabling client clipboard. + // 1) Disable clipboard + // 2) Copy text "a" + // 3) Enable clipboard + // 4) Switch to another app + // 5) Switch back to the app + // 6) "a" should not be sent to the client, because it's copied before enabling clipboard + // + // It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`, + // though the `lastUpdatedClipData` will be set to null once. + private var lastUpdatedClipData: ClipData? = null + private var isClientEnabled = true; + private var _isCaptureStarted = false; + + val isCaptureStarted: Boolean + get() = _isCaptureStarted + + fun checkPrimaryClip(isClient: Boolean) { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + // Only handle the first item in the clipboard for now. + val clip = clipData.getItemAt(0) + // Ignore the `isClipboardDataEqual()` check if it's a host operation. + // Because it's an action manually triggered by the user. + if (isClient) { + if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { + Log.d(logTag, "Clipboard data is the same as last update, ignore") + return + } + } + val mimeTypeCount = clipData.description.getMimeTypeCount() + val mimeTypes = mutableListOf() + for (i in 0 until mimeTypeCount) { + mimeTypes.add(clipData.description.getMimeType(i)) + } + var text: CharSequence? = null; + var html: String? = null; + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + text = clip?.text + } + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { + text = clip?.text + html = clip?.htmlText + } + var count = 0 + val clips = MultiClipboards.newBuilder() + if (text != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString()) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build()) + count++ + } + if (html != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(html) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build()) + count++ + } + if (count > 0) { + val clipsBytes = clips.build().toByteArray() + val isClientFlag = if (isClient) 1 else 0 + val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply { + put(isClientFlag.toByte()) + put(clipsBytes) + } + clipsBuf.flip() + lastUpdatedClipData = clipData + Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote") + FFI.onClipboardUpdate(clipsBuf) + } + } + } + + private fun isSupportedMimeType(mimeType: String): Boolean { + return supportedMimeTypes.contains(mimeType) + } + + private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean { + if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) { + return false + } + val mimeTypeCount = left.description.getMimeTypeCount() + for (i in 0 until mimeTypeCount) { + if (left.description.getMimeType(i) != right.description.getMimeType(i)) { + return false + } + } + + if (left.itemCount != right.itemCount) { + return false + } + for (i in 0 until left.itemCount) { + val mimeType = left.description.getMimeType(i) + if (!isSupportedMimeType(mimeType)) { + continue + } + val leftItem = left.getItemAt(i) + val rightItem = right.getItemAt(i) + if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) { + if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) { + return false + } + } + } + return true + } + + fun setCaptureStarted(started: Boolean) { + _isCaptureStarted = started + } + + @Keep + fun rustEnableClientClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableClientClipboard: enable: $enable") + isClientEnabled = enable + lastUpdatedClipData = null + } + + fun syncClipboard(isClient: Boolean) { + Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled") + if (isClient && !isClientEnabled) { + return + } + checkPrimaryClip(isClient) + } + + @Keep + fun rustUpdateClipboard(clips: ByteArray) { + val clips = MultiClipboards.parseFrom(clips) + var mimeTypes = mutableListOf() + var text: String? = null + var html: String? = null + for (clip in clips.getClipboardsList()) { + when (clip.format) { + ClipboardFormat.Text -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN) + text = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.Html -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML) + html = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.ImageRgba -> { + } + ClipboardFormat.ImagePng -> { + } + else -> { + Log.e(logTag, "Unsupported clipboard format: ${clip.format}") + } + } + } + + val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray()) + var item: ClipData.Item? = null + if (text == null) { + Log.e(logTag, "No text content in clipboard") + return + } else { + if (html == null) { + item = ClipData.Item(text) + } else { + item = ClipData.Item(text, html) + } + } + if (item == null) { + Log.e(logTag, "No item in clipboard") + return + } + val clipData = ClipData(clipDescription, item) + lastUpdatedClipData = clipData + clipboardManager.setPrimaryClip(clipData) + } +} diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index a7573bbf9eed..9f0f0216b727 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -5,12 +5,15 @@ package ffi import android.content.Context import java.nio.ByteBuffer +import com.carriez.flutter_hbb.RdClipboardManager + object FFI { init { System.loadLibrary("rustdesk") } external fun init(ctx: Context) + external fun setClipboardManager(clipboardManager: RdClipboardManager) external fun startServer(app_dir: String, custom_client_config: String) external fun startService() external fun onVideoFrameUpdate(buf: ByteBuffer) @@ -20,4 +23,6 @@ object FFI { external fun setFrameRawEnable(name: String, value: Boolean) external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String -} \ No newline at end of file + external fun onClipboardUpdate(clips: ByteBuffer) + external fun isServiceClipboardEnabled(): Boolean +} diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index c6a77f36b171..401bea0096e2 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,18 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.14' - } -} - allprojects { repositories { google() @@ -24,6 +9,8 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { project.evaluationDependsOn(':app') } diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index cc5527d781a7..cb576305fbe9 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle index 44e62bcf06ae..c5fb685a1614 100644 --- a/flutter/android/settings.gradle +++ b/flutter/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +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 +} + +include ":app" diff --git a/flutter/assets/message_24dp_5F6368.svg b/flutter/assets/message_24dp_5F6368.svg new file mode 100644 index 000000000000..5347a3d2d652 --- /dev/null +++ b/flutter/assets/message_24dp_5F6368.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index e4210477e360..64fb9dad2a90 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -68,6 +68,7 @@ function build { pushd "$SCRIPTDIR/.." $VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed" popd + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies" if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 2e0a20b6db60..ecfb444efea5 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -1,7 +1,5 @@ #!/bin/bash -set -x - # # Script to build F-Droid release of RustDesk # @@ -23,6 +21,43 @@ set -x # + build: perform actual build of APK file # +# Start of functions + +# Install Flutter of version `VERSION` from Github repository +# into directory `FLUTTER_DIR` and apply patches if needed + +prepare_flutter() { + VERSION="${1}" + FLUTTER_DIR="${2}" + + if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then + git clone https://github.com/flutter/flutter "${FLUTTER_DIR}" + fi + + pushd "${FLUTTER_DIR}" + + git restore . + git checkout "${VERSION}" + + # Patch flutter + + if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then + git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" + fi + + flutter config --no-analytics + + popd # ${FLUTTER_DIR} +} + +# Start of script + +set -x + +# Note current working directory as root dir for patches + +ROOTDIR="${PWD}" + # Parse command-line arguments VERNAME="${1}" @@ -101,18 +136,31 @@ prebuild) .env.CARGO_NDK_VERSION \ .github/workflows/flutter-build.yml)" + # Flutter used to compile main Rustdesk library + FLUTTER_VERSION="$(yq -r \ .env.ANDROID_FLUTTER_VERSION \ .github/workflows/flutter-build.yml)" + if [ -z "${FLUTTER_VERSION}" ]; then FLUTTER_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ .github/workflows/flutter-build.yml)" fi + # 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)" + FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_RUST_BRIDGE_VERSION \ - .github/workflows/flutter-build.yml)" + .github/workflows/bridge.yml)" NDK_VERSION="$(yq -r \ .env.NDK_VERSION \ @@ -127,6 +175,7 @@ prebuild) .github/workflows/flutter-build.yml)" if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] || + [ -z "${FLUTTER_BRIDGE_VERSION}" ] || [ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] || [ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] || [ -z "${VCPKG_COMMIT_ID}" ]; then @@ -163,24 +212,6 @@ prebuild) sdkmanager --install "ndk;${NDK_VERSION}" fi - # Install Flutter - - if [ ! -f "${HOME}/flutter/bin/flutter" ]; then - pushd "${HOME}" - - git clone https://github.com/flutter/flutter - - pushd flutter - - git reset --hard "${FLUTTER_VERSION}" - - flutter config --no-analytics - - popd # flutter - - popd # ${HOME} - fi - # Install Rust if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then @@ -205,14 +236,19 @@ prebuild) cargo install \ cargo-ndk \ - --version "${CARGO_NDK_VERSION}" + --version "${CARGO_NDK_VERSION}" \ + --locked # 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" + --features "uuid" \ + --locked # Populate native vcpkg dependencies @@ -275,12 +311,66 @@ prebuild) git apply res/fdroid/patches/*.patch + # If Flutter version used to generate bridge files differs from Flutter + # version used to compile Rustdesk library, generate bridge using the + # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later + + if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then + # Install Flutter bridge version + + prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" + + # Save changes + + git add . + + # Edit pubspec to make flutter bridge version work + + sed \ + -i \ + -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ + flutter/pubspec.yaml + + # Download Flutter dependencies + + pushd flutter + + flutter clean + flutter packages pub get + + popd # flutter + + # Generate FFI bindings + + flutter_rust_bridge_codegen \ + --rust-input ./src/flutter_ffi.rs \ + --dart-output ./flutter/lib/generated_bridge.dart + + # Add bridge files to save-list + + git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.* + + # Restore everything + + git checkout '*' + git clean -dffx + git reset + fi + + # Install Flutter version for RustDesk library build + + prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" + + # gms is not in thoes files now, but we still keep the following line for future reference(maybe). + sed \ -i \ -e '/gms/d' \ flutter/android/build.gradle \ flutter/android/app/build.gradle + # `firebase_analytics` is not in these files now, but we still keep the following lines. + sed \ -i \ -e '/firebase_analytics/d' \ @@ -296,33 +386,6 @@ prebuild) -e '/firebase/Id' \ flutter/lib/main.dart - if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then - # Fix for android 3.13.9 - # https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862 - - sed \ - -i \ - -e 's/uni_links_desktop/#uni_links_desktop/g' \ - flutter/pubspec.yaml - - set -- - - while read -r _1; do - set -- "$@" "${_1}" - done 0<<.a -$(find flutter/lib/ -type f -name "*dart*") -.a - - sed \ - -i \ - -e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \ - "$@" - - set -- - fi - - sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient - ;; build) # build: perform actual build of APK file @@ -334,9 +397,12 @@ build) # '.github/workflows/flutter-build.yml' # + # Flutter used to compile main Rustdesk library + FLUTTER_VERSION="$(yq -r \ .env.ANDROID_FLUTTER_VERSION \ .github/workflows/flutter-build.yml)" + if [ -z "${FLUTTER_VERSION}" ]; then FLUTTER_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ @@ -372,16 +438,11 @@ build) pushd flutter + flutter clean flutter packages pub get popd # flutter - # Generate FFI bindings - - flutter_rust_bridge_codegen \ - --rust-input ./src/flutter_ffi.rs \ - --dart-output ./flutter/lib/generated_bridge.dart - # Build host android deps bash flutter/build_android_deps.sh "${ANDROID_ABI}" diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh index a6468a0a8fb7..50f2f0056b41 100755 --- a/flutter/build_ios.sh +++ b/flutter/build_ios.sh @@ -2,4 +2,6 @@ # https://docs.flutter.dev/deployment/ios # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info # 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 flutter build ipa --release diff --git a/flutter/deploy.sh b/flutter/deploy.sh deleted file mode 100755 index f6826fd8720d..000000000000 --- 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/Flutter/AppFrameworkInfo.plist b/flutter/ios/Flutter/AppFrameworkInfo.plist index 7c5696400627..1dc6cf7652ba 100644 --- a/flutter/ios/Flutter/AppFrameworkInfo.plist +++ b/flutter/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/flutter/ios/Podfile b/flutter/ios/Podfile index 321d7132cfb3..b71c436f2291 100644 --- a/flutter/ios/Podfile +++ b/flutter/ios/Podfile @@ -1,10 +1,7 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' - # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' -platform :ios, '12.0' +platform :ios, '13.0' project 'Runner', { 'Debug' => :debug, diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index 6cb5c9cff4c5..c9e9f9a2ffdd 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -133,10 +133,10 @@ SPEC CHECKSUMS: sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 -PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 +PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c COCOAPODS: 1.15.2 diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index acc2a09e7475..36dc89ea8a96 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -347,7 +347,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -491,7 +491,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -541,7 +541,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 89e443af6d8b..d9333b7067be 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh index 579baaa6ddae..2d8410c7a4ef 100755 --- a/flutter/ios_arm64.sh +++ b/flutter/ios_arm64.sh @@ -1,2 +1,4 @@ #!/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 eba626343d03..2fde813bcdba 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; +import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -50,6 +51,9 @@ final isLinux = isLinux_; final isDesktop = isDesktop_; final isWeb = isWeb_; final isWebDesktop = isWebDesktop_; +final isWebOnWindows = isWebOnWindows_; +final isWebOnLinux = isWebOnLinux_; +final isWebOnMacOs = isWebOnMacOS_; var isMobile = isAndroid || isIOS; var version = ''; int androidVersion = 0; @@ -60,6 +64,9 @@ int androidVersion = 0; // So we need to use this flag to enable/disable resizable. bool _linuxWindowResizable = true; +// Only used on Windows(window manager). +bool _ignoreDevicePixelRatio = true; + /// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -347,6 +354,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white, + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -442,6 +452,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E), + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -545,9 +558,9 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) async { + static Future changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); - if (desktopType == DesktopType.main || isAndroid || isIOS) { + if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) { if (mode == ThemeMode.system) { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: defaultOptionTheme); @@ -555,7 +568,7 @@ class MyTheme { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - await bind.mainChangeTheme(dark: mode.toShortString()); + if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString()); // Synchronize the window theme of the system. updateSystemWindowTheme(); } @@ -629,10 +642,30 @@ List supportedLocales = const [ Locale('da'), Locale('eo'), Locale('tr'), - Locale('vi'), - Locale('pl'), Locale('kz'), Locale('es'), + Locale('nl'), + Locale('nb'), + Locale('et'), + Locale('eu'), + Locale('bg'), + Locale('be'), + Locale('vn'), + Locale('uk'), + Locale('fa'), + Locale('ca'), + Locale('el'), + Locale('sv'), + Locale('sq'), + Locale('sr'), + Locale('th'), + Locale('sl'), + Locale('ro'), + Locale('lt'), + Locale('lv'), + Locale('ar'), + Locale('he'), + Locale('hr'), ]; String formatDurationToTime(Duration duration) { @@ -651,10 +684,12 @@ closeConnection({String? id}) { overlays: SystemUiOverlay.values); gFFI.chatModel.hideChatOverlay(); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; }(); } else { if (isWeb) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; } else { final controller = Get.find(); controller.closeBy(id); @@ -1057,6 +1092,49 @@ class CustomAlertDialog extends StatelessWidget { } } +Widget createDialogContent(String text) { + final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); + final List spans = []; + int start = 0; + bool hasLink = false; + + linkRegExp.allMatches(text).forEach((match) { + hasLink = true; + if (match.start > start) { + spans.add(TextSpan(text: text.substring(start, match.start))); + } + spans.add(TextSpan( + text: match.group(0) ?? '', + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + String linkText = match.group(0) ?? ''; + linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), ''); + launchUrl(Uri.parse(linkText)); + }, + )); + start = match.end; + }); + + if (start < text.length) { + spans.add(TextSpan(text: text.substring(start))); + } + + if (!hasLink) { + return SelectableText(text, style: const TextStyle(fontSize: 15)); + } + + return SelectableText.rich( + TextSpan( + style: TextStyle(color: Colors.black, fontSize: 15), + children: spans, + ), + ); +} + void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { @@ -1099,33 +1177,21 @@ void msgBox(SessionID sessionId, String type, String title, String text, dialogManager.dismissAll(); })); } - if (reconnect != null && title == "Connection Error") { + if (reconnect != null && + title == "Connection Error" && + reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = reconnectTimeout != null - ? Obx(() => _ReconnectCountDownButton( - second: reconnectTimeout, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - )) - : Obx( - () => dialogButton( - 'Reconnect', - isOutline: true, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - ), - ); + final button = Obx(() => _ReconnectCountDownButton( + second: reconnectTimeout, + onPressed: enabled.isTrue + ? () { + // Disable the button + enabled.value = false; + reconnect(dialogManager, sessionId, false); + } + : null, + )); buttons.insert(0, button); } if (link.isNotEmpty) { @@ -1214,7 +1280,7 @@ Widget msgboxContent(String type, String title, String text) { translate(title), style: TextStyle(fontSize: 21), ).marginOnly(bottom: 10), - Text(translateText(text), style: const TextStyle(fontSize: 15)), + createDialogContent(translateText(text)), ], ), ), @@ -1548,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) { return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } -class OffsetDevicePixelRatio { - Offset offset; - final double devicePixelRatio; - OffsetDevicePixelRatio(this.offset, this.devicePixelRatio); -} - class LastWindowPosition { double? width; double? height; @@ -1636,8 +1696,10 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { if (isFullscreen || isMaximized) { setPreFrame(); } else { - position = await windowManager.getPosition(); - sz = await windowManager.getSize(); + position = await windowManager.getPosition( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + sz = await windowManager.getSize( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); } break; default: @@ -1755,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) { } /// return null means center -Future _adjustRestoreMainWindowOffset( +Future _adjustRestoreMainWindowOffset( double? left, double? top, double? width, @@ -1769,13 +1831,9 @@ Future _adjustRestoreMainWindowOffset( double? frameTop; double? frameRight; double? frameBottom; - double devicePixelRatio = 1.0; if (isDesktop || isWebDesktop) { for (final screen in await window_size.getScreenList()) { - if (isPointInRect(Offset(left, top), screen.visibleFrame)) { - devicePixelRatio = screen.scaleFactor; - } frameLeft = frameLeft == null ? screen.visibleFrame.left : min(screen.visibleFrame.left, frameLeft); @@ -1809,7 +1867,7 @@ Future _adjustRestoreMainWindowOffset( top < frameTop!) { return null; } else { - return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio); + return Offset(left, top); } } @@ -1869,47 +1927,23 @@ Future restoreWindowPosition(WindowType type, } final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height); - final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset( + final offsetLeftTop = await _adjustRestoreMainWindowOffset( lpos.offsetWidth, lpos.offsetHeight, size.width, size.height, ); debugPrint( - "restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); + "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); switch (type) { case WindowType.Main: - // https://github.com/rustdesk/rustdesk/issues/8038 - // `setBounds()` in `window_manager` will use the current devicePixelRatio. - // So we need to adjust the offset by the scale factor. - // https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701 - if (isWindows) { - double? curDevicePixelRatio; - Offset curPos = await windowManager.getPosition(); - for (final screen in await window_size.getScreenList()) { - if (isPointInRect(curPos, screen.visibleFrame)) { - curDevicePixelRatio = screen.scaleFactor; - } - } - if (curDevicePixelRatio != null && - curDevicePixelRatio != 0 && - offsetDevicePixelRatio != null) { - if (offsetDevicePixelRatio.devicePixelRatio != 0) { - final scale = - offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio; - offsetDevicePixelRatio.offset = - offsetDevicePixelRatio.offset.scale(scale, scale); - debugPrint( - "restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale"); - } - } - } restorePos() async { - if (offsetDevicePixelRatio == null) { + if (offsetLeftTop == null) { await windowManager.center(); } else { - await windowManager.setPosition(offsetDevicePixelRatio.offset); + await windowManager.setPosition(offsetLeftTop, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); } } if (lpos.isMaximized == true) { @@ -1918,20 +1952,39 @@ Future restoreWindowPosition(WindowType type, await windowManager.maximize(); } } else { - if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) { - await windowManager.setSize(size); + final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly(); + if (isWindows) { + if (storeSize) { + // We need to set the window size first to avoid the incorrect size in some special cases. + // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI. + // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor. + // After restoring, the size will be incorrect. + // See known issue in https://github.com/rustdesk/rustdesk/pull/9840 + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + } else { + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); } - await restorePos(); } return true; default: final wc = WindowController.fromWindowId(windowId!); restoreFrame() async { - if (offsetDevicePixelRatio == null) { + if (offsetLeftTop == null) { await wc.center(); } else { - final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx, - offsetDevicePixelRatio.offset.dy, size.width, size.height); + final frame = Rect.fromLTWH( + offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height); await wc.setFrame(frame); } } @@ -1963,6 +2016,8 @@ Future restoreWindowPosition(WindowType type, return false; } +var webInitialLink = ""; + /// Initialize uni links for macos/windows /// /// [Availability] @@ -1979,7 +2034,12 @@ Future initUniLinks() async { if (initialLink == null || initialLink.isEmpty) { return false; } - return handleUriLink(uriString: initialLink); + if (isWeb) { + webInitialLink = initialLink; + return false; + } else { + return handleUriLink(uriString: initialLink); + } } catch (err) { debugPrintStack(label: "$err"); return false; @@ -1992,7 +2052,7 @@ Future initUniLinks() async { /// /// Returns a [StreamSubscription] which can listen the uni links. StreamSubscription? listenUniLinks({handleByFlutter = true}) { - if (isLinux) { + if (isLinux || isWeb) { return null; } @@ -2185,7 +2245,10 @@ List? urlLinkToCmdArgs(Uri uri) { } } - var key = uri.queryParameters["key"]; + var queryParameters = + uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v)); + + var key = queryParameters["key"]; if (id != null) { if (key != null) { id = "$id?key=$key"; @@ -2194,7 +2257,7 @@ List? urlLinkToCmdArgs(Uri uri) { if (isMobile) { if (id != null) { - final forceRelay = uri.queryParameters["relay"] != null; + final forceRelay = queryParameters["relay"] != null; connect(Get.context!, id, forceRelay: forceRelay); return null; } @@ -2204,7 +2267,7 @@ List? urlLinkToCmdArgs(Uri uri) { if (command != null && id != null) { args.add(command); args.add(id); - var param = uri.queryParameters; + var param = queryParameters; String? password = param["password"]; if (password != null) args.addAll(['--password', password]); String? switch_uuid = param["switch_uuid"]; @@ -2222,16 +2285,19 @@ connectMainDesktop(String id, required bool isRDP, bool? forceRelay, String? password, + String? connToken, bool? isSharedPassword}) async { if (isFileTransfer) { await rustDeskWinManager.newFileTransfer(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 { await rustDeskWinManager.newRemoteDesktop(id, @@ -2251,6 +2317,7 @@ connect(BuildContext context, String id, bool isRDP = false, bool forceRelay = false, String? password, + String? connToken, bool? isSharedPassword}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { @@ -2292,24 +2359,40 @@ connect(BuildContext context, String id, 'password': password, 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, + 'connToken': connToken, }); } } else { if (isFileTransfer) { - if (!await AndroidPermissionManager.check(kManageExternalStorage)) { - if (!await AndroidPermissionManager.request(kManageExternalStorage)) { - return; + if (isAndroid) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { + return; + } } } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage( - id: id, password: password, isSharedPassword: isSharedPassword), - ), - ); + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_file_manager.FileManagerPage( + id: id, + password: password, + isSharedPassword: isSharedPassword), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage( + id: id, password: password, isSharedPassword: isSharedPassword), + ), + ); + } } else { - if (isWebDesktop) { + if (isWeb) { Navigator.push( context, MaterialPageRoute( @@ -2333,6 +2416,7 @@ connect(BuildContext context, String id, ); } } + stateGlobal.isInMainPage = false; } FocusScopeNode currentFocus = FocusScope.of(context); @@ -2417,9 +2501,20 @@ Future onActiveWindowChanged() async { // But the app will not close. // // No idea why we need to delay here, `terminate()` itself is also an async function. - Future.delayed(Duration.zero, () { - RdPlatformChannel.instance.terminate(); - }); + // + // A quick workaround, use `Timer.periodic` to avoid the app not closing. + // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()` + // may not work since `Flutter 3.24.4`, see the following logs. + // A delay will allow the app to close. + // + //``` + // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid. + // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2) + // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle. + // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2). + // ``` + periodic_immediate( + Duration(milliseconds: 30), RdPlatformChannel.instance.terminate); } } } @@ -2512,7 +2607,7 @@ class ServerConfig { config['relay'] = relayServer.trim(); config['api'] = apiServer.trim(); config['key'] = key.trim(); - return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits)) .split('') .reversed .join(); @@ -2635,30 +2730,6 @@ Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } -class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { - /// Creates scroll physics that does not let the user scroll. - const DraggableNeverScrollableScrollPhysics({super.parent}); - - @override - DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { - return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); - } - - @override - bool shouldAcceptUserOffset(ScrollMetrics position) { - // TODO: find a better solution to check if the offset change is caused by the scrollbar. - // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. - if (position is ScrollPositionWithSingleContext) { - // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - return position.activity is IdleScrollActivity; - } - return false; - } - - @override - bool get allowImplicitScrolling => false; -} - Widget futureBuilder( {required Future? future, required Widget Function(dynamic data) hasData}) { return FutureBuilder( @@ -2738,7 +2809,7 @@ Widget buildRemoteBlock( onExit: (event) => block.value = false, child: Stack(children: [ // scope block tab - FocusScope(child: child, canRequestFocus: !block.value), + preventMouseKeyBuilder(child: child, block: block.value), // mask block click, cm not block click and still use check_click_time to avoid block local click if (mask) Offstage( @@ -2750,6 +2821,11 @@ Widget buildRemoteBlock( )); } +Widget preventMouseKeyBuilder({required Widget child, required bool block}) { + return ExcludeFocus( + excluding: block, child: AbsorbPointer(child: child, absorbing: block)); +} + Widget unreadMessageCountBuilder(RxInt? count, {double? size, double? fontSize}) { return Obx(() => Offstage( @@ -2813,7 +2889,7 @@ Widget buildErrorBanner(BuildContext context, alignment: Alignment.centerLeft, child: Tooltip( message: translate(err.value), - child: Text( + child: SelectableText( translate(err.value), ), )).marginSymmetric(vertical: 2), @@ -3082,9 +3158,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { importConfig(List? controllers, List? errMsgs, String? text) { + text = text?.trim(); if (text != null && text.isNotEmpty) { try { final sc = ServerConfig.decode(text); + if (isWeb || isIOS) { + sc.relayServer = ''; + } if (sc.idServer.isNotEmpty) { Future success = setServerConfig(controllers, errMsgs, sc); success.then((value) { @@ -3429,7 +3509,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) { ); } -get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize; +get windowResizeEdgeSize => + isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize; // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable. // See _linuxWindowResizable for more details. @@ -3490,3 +3571,70 @@ disableWindowMovable(int? windowId) { WindowController.fromWindowId(windowId).setMovable(false); } } + +Widget netWorkErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(translate("network_error_tip")), + ElevatedButton( + onPressed: gFFI.userModel.refreshCurrentUser, + child: Text(translate("Retry"))) + .marginSymmetric(vertical: 16), + SelectableText(gFFI.userModel.networkError.value, + style: TextStyle(fontSize: 11, color: Colors.red)), + ], + )); +} + +List? get windowManagerEnableResizeEdges => isWindows + ? [ + ResizeEdge.topLeft, + ResizeEdge.top, + ResizeEdge.topRight, + ] + : null; + +List? get subWindowManagerEnableResizeEdges => isWindows + ? [ + SubWindowResizeEdge.topLeft, + SubWindowResizeEdge.top, + SubWindowResizeEdge.topRight, + ] + : null; + +void earlyAssert() { + assert('\1' == '1'); +} + +void checkUpdate() { + if (!isWeb) { + if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + stateGlobal.updateUrl.value = evt['url']; + } + }); + Timer(const Duration(seconds: 1), () async { + bind.mainGetSoftwareUpdateUrl(); + }); + } + } +} + +// 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; + } + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 52b2e3d6296a..deed97bb30bd 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:bot_toast/bot_toast.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; @@ -41,6 +43,8 @@ class _AddressBookState extends State { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); } else { return Column( children: [ @@ -59,15 +63,16 @@ class _AddressBookState extends State { retry: null, // remove retry close: () => gFFI.abModel.currentAbPushError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildAddressBookDesktop() - : _buildAddressBookMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildAddressBookPortrait() + : _buildAddressBookLandscape()), + ), ], ); } }); - Widget _buildAddressBookDesktop() { + Widget _buildAddressBookLandscape() { return Row( children: [ Offstage( @@ -104,7 +109,7 @@ class _AddressBookState extends State { ); } - Widget _buildAddressBookMobile() { + Widget _buildAddressBookPortrait() { const padding = 8.0; return Column( children: [ @@ -237,14 +242,15 @@ class _AddressBookState extends State { bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); } }, - customButton: Container( - height: isDesktop ? 48 : 40, - child: Row(children: [ - Expanded( - child: buildItem(gFFI.abModel.currentName.value, button: true)), - Icon(Icons.arrow_drop_down), - ]), - ), + customButton: Obx(() => Container( + height: stateGlobal.isPortrait.isFalse ? 48 : 40, + child: Row(children: [ + Expanded( + child: + buildItem(gFFI.abModel.currentName.value, button: true)), + Icon(Icons.arrow_drop_down), + ]), + )), underline: Container( height: 0.7, color: Theme.of(context).dividerColor.withOpacity(0.1), @@ -280,7 +286,7 @@ class _AddressBookState extends State { borderRadius: BorderRadius.circular(8), ), ), - ), + ).workaroundFreezeLinuxMint(), ), searchMatchFn: (item, searchValue) { return item.value @@ -311,13 +317,14 @@ class _AddressBookState extends State { Widget _buildTags() { return Obx(() { - final List tags; + List tags; if (gFFI.abModel.sortTags.value) { tags = gFFI.abModel.currentAbTags.toList(); tags.sort(); } else { - tags = gFFI.abModel.currentAbTags; + tags = gFFI.abModel.currentAbTags.toList(); } + tags = [kUntagged, ...tags].toList(); final editPermission = gFFI.abModel.current.canWrite(); tagBuilder(String e) { return AddressBookTag( @@ -333,8 +340,8 @@ class _AddressBookState extends State { showActionMenu: editPermission); } - final gridView = DynamicGridView.builder( - shrinkWrap: isMobile, + gridView(bool isPortrait) => DynamicGridView.builder( + shrinkWrap: isPortrait, gridDelegate: SliverGridDelegateWithWrapping(), itemCount: tags.length, itemBuilder: (BuildContext context, int index) { @@ -342,9 +349,9 @@ class _AddressBookState extends State { return tagBuilder(e); }); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? gridView - : LimitedBox(maxHeight: maxHeight, child: gridView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? gridView(false) + : LimitedBox(maxHeight: maxHeight, child: gridView(true))); }); } @@ -354,7 +361,6 @@ class _AddressBookState extends State { alignment: Alignment.topLeft, child: AddressBookPeersView( menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.abModel.currentAbPeers, )), ); } @@ -504,20 +510,21 @@ class _AddressBookState extends State { double marginBottom = 4; row({required Widget lable, required Widget input}) { - return Row( - children: [ - !isMobile - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: lable.marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 200), - child: input), - ), - ], - ).marginOnly(bottom: !isMobile ? 8 : 0); + makeChild(bool isPortrait) => Row( + children: [ + !isPortrait + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: lable.marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: input), + ), + ], + ).marginOnly(bottom: !isPortrait ? 8 : 0); + return Obx(() => makeChild(stateGlobal.isPortrait.isTrue)); } return CustomAlertDialog( @@ -540,24 +547,29 @@ class _AddressBookState extends State { ), ], ), - input: TextField( - controller: idController, - inputFormatters: [IDTextInputFormatter()], - decoration: InputDecoration( - labelText: !isMobile ? null : translate('ID'), - errorText: errorMsg, - errorMaxLines: 5), - )), + input: Obx(() => TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('ID'), + errorText: errorMsg, + errorMaxLines: 5), + ).workaroundFreezeLinuxMint())), row( lable: Text( translate('Alias'), style: style, ), - input: TextField( - controller: aliasController, - decoration: InputDecoration( - labelText: !isMobile ? null : translate('Alias'), - )), + input: Obx(() => TextField( + controller: aliasController, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Alias'), + ), + ).workaroundFreezeLinuxMint()), ), if (isCurrentAbShared) row( @@ -565,24 +577,28 @@ class _AddressBookState extends State { translate('Password'), style: style, ), - input: TextField( - controller: passwordController, - obscureText: !passwordVisible, - decoration: InputDecoration( - labelText: !isMobile ? null : translate('Password'), - suffixIcon: IconButton( - icon: Icon( - passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: MyTheme.lightTheme.primaryColor), - onPressed: () { - setState(() { - passwordVisible = !passwordVisible; - }); - }, + input: Obx( + () => TextField( + controller: passwordController, + obscureText: !passwordVisible, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Password'), + suffixIcon: IconButton( + icon: Icon( + passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), ), - ), + ).workaroundFreezeLinuxMint(), )), if (gFFI.abModel.currentAbTags.isNotEmpty) Align( @@ -655,6 +671,14 @@ class _AddressBookState extends State { } else { final tags = field.trim().split(RegExp(r"[\s,;\n]+")); field = tags.join(','); + for (var t in [kUntagged, translate(kUntagged)]) { + if (tags.contains(t)) { + BotToast.showText( + contentColor: Colors.red, text: 'Tag name cannot be "$t"'); + isInProgress = false; + return; + } + } gFFI.abModel.addTags(tags); // final currentPeers } @@ -680,7 +704,7 @@ class _AddressBookState extends State { ), controller: controller, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ), @@ -727,12 +751,14 @@ class AddressBookTag extends StatelessWidget { } const double radius = 8; + final isUnTagged = name == kUntagged; + final showAction = showActionMenu && !isUnTagged; return GestureDetector( onTap: onTap, - onTapDown: showActionMenu ? setPosition : null, - onSecondaryTapDown: showActionMenu ? setPosition : null, - onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, - onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, + onTapDown: showAction ? setPosition : null, + onSecondaryTapDown: showAction ? setPosition : null, + onSecondaryTap: showAction ? () => _showMenu(context, pos) : null, + onLongPress: showAction ? () => _showMenu(context, pos) : null, child: Obx(() => Container( decoration: BoxDecoration( color: tags.contains(name) @@ -744,17 +770,18 @@ class AddressBookTag extends StatelessWidget { child: IntrinsicWidth( child: Row( children: [ - Container( - width: radius, - height: radius, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: tags.contains(name) - ? Colors.white - : gFFI.abModel.getCurrentAbTagColor(name)), - ).marginOnly(right: radius / 2), + if (!isUnTagged) + Container( + width: radius, + height: radius, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: tags.contains(name) + ? Colors.white + : gFFI.abModel.getCurrentAbTagColor(name)), + ).marginOnly(right: radius / 2), Expanded( - child: Text(name, + child: Text(isUnTagged ? translate(name) : name, style: TextStyle( overflow: TextOverflow.ellipsis, color: tags.contains(name) ? Colors.white : null)), diff --git a/flutter/lib/common/widgets/audio_input.dart b/flutter/lib/common/widgets/audio_input.dart index 1db4391270cb..1f8f1a8b94e6 100644 --- a/flutter/lib/common/widgets/audio_input.dart +++ b/flutter/lib/common/widgets/audio_input.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -const _kWindowsSystemSound = 'System Sound'; +const _kSystemSound = 'System Sound'; typedef AudioINputSetDevice = void Function(String device); typedef AudioInputBuilder = Widget Function( @@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget { : super(key: key); static String getDefault() { - if (isWindows) return translate('System Sound'); + if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound); return ''; } @@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget { static Future> getDevicesInfo( bool isCm, bool isVoiceCall) async { List devices = (await bind.mainGetSoundInputs()).toList(); - if (isWindows) { - devices.insert(0, translate(_kWindowsSystemSound)); + if (bind.mainAudioSupportLoopback()) { + devices.insert(0, translate(_kSystemSound)); } String current = await getValue(isCm, isVoiceCall); return {'devices': devices, 'current': current}; diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 07a11904da17..978d053df4be 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State { .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : widget.peer.tags.isNotEmpty ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index b6611d3ede39..4b0954d40b19 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/connection_page_title.dart b/flutter/lib/common/widgets/connection_page_title.dart new file mode 100644 index 000000000000..ba03c2656960 --- /dev/null +++ b/flutter/lib/common/widgets/connection_page_title.dart @@ -0,0 +1,38 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +Widget getConnectionPageTitle(BuildContext context, bool isWeb) { + return Row( + children: [ + Expanded( + child: Row( + children: [ + AutoSizeText( + translate('Control Remote Desktop'), + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), + ).marginOnly(right: 4), + Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"), + child: Icon( + Icons.help_outline_outlined, + size: 16, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + ), + ), + ], + )), + ], + ); +} diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart index 99ece2434bf4..dafc23b448b9 100644 --- a/flutter/lib/common/widgets/custom_password.dart +++ b/flutter/lib/common/widgets/custom_password.dart @@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule { String get name => translate('uppercase'); @override bool validate(String value) { - return value.contains(RegExp(r'[A-Z]')); + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toUpperCase() == character && + character.toLowerCase() != character; + }); } } @@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule { @override bool validate(String value) { - return value.contains(RegExp(r'[a-z]')); + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toLowerCase() == character && + character.toUpperCase() != character; + }); } } diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index f140a68b0779..0fb8d552d7ab 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -139,7 +140,7 @@ void changeIdDialog() { msg = ''; }); }, - ), + ).workaroundFreezeLinuxMint(), const SizedBox( height: 8.0, ), @@ -200,13 +201,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(), ), ], ), @@ -286,22 +288,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(), ), ], ), @@ -334,21 +337,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(), ), ], ), @@ -380,6 +384,7 @@ class DialogTextField extends StatelessWidget { final FocusNode? focusNode; final TextInputType? keyboardType; final List? inputFormatters; + final int? maxLength; static const kUsernameTitle = 'Username'; static const kUsernameIcon = Icon(Icons.account_circle_outlined); @@ -397,6 +402,7 @@ class DialogTextField extends StatelessWidget { this.hintText, this.keyboardType, this.inputFormatters, + this.maxLength, required this.title, required this.controller}) : super(key: key); @@ -423,7 +429,8 @@ class DialogTextField extends StatelessWidget { obscureText: obscureText, keyboardType: keyboardType, inputFormatters: inputFormatters, - ), + maxLength: maxLength, + ).workaroundFreezeLinuxMint(), ), ], ).paddingSymmetric(vertical: 4.0); @@ -679,6 +686,8 @@ class PasswordWidget extends StatefulWidget { this.reRequestFocus = false, this.hintText, this.errorText, + this.title, + this.maxLength, }) : super(key: key); final TextEditingController controller; @@ -686,6 +695,8 @@ class PasswordWidget extends StatefulWidget { final bool reRequestFocus; final String? hintText; final String? errorText; + final String? title; + final int? maxLength; @override State createState() => _PasswordWidgetState(); @@ -729,7 +740,7 @@ class _PasswordWidgetState extends State { @override Widget build(BuildContext context) { return DialogTextField( - title: translate(DialogTextField.kPasswordTitle), + title: translate(widget.title ?? DialogTextField.kPasswordTitle), hintText: translate(widget.hintText ?? 'Enter your password'), controller: widget.controller, prefixIcon: DialogTextField.kPasswordIcon, @@ -748,6 +759,7 @@ class _PasswordWidgetState extends State { obscureText: !_passwordVisible, errorText: widget.errorText, focusNode: _focusNode, + maxLength: widget.maxLength, ); } } @@ -1121,7 +1133,7 @@ void showRequestElevationDialog( errorText: errPwd.isEmpty ? null : errPwd.value, ), ], - ).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), + ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0), ).marginOnly(top: 10), ], ), @@ -1492,7 +1504,7 @@ showAuditDialog(FFI ffi) async { maxLength: 256, controller: controller, focusNode: focusNode, - )), + ).workaroundFreezeLinuxMint()), actions: [ dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('OK', onPressed: submit) @@ -1739,7 +1751,7 @@ void renameDialog( autofocus: true, decoration: InputDecoration(labelText: translate('Name')), validator: validator, - ), + ).workaroundFreezeLinuxMint(), ), ), // NOT use Offstage to wrap LinearProgressIndicator @@ -1799,7 +1811,7 @@ void changeBot({Function()? callback}) async { decoration: InputDecoration( hintText: translate('Token'), ), - ); + ).workaroundFreezeLinuxMint(); return CustomAlertDialog( title: Text(translate("Telegram bot")), @@ -1829,6 +1841,7 @@ void changeBot({Function()? callback}) async { void change2fa({Function()? callback}) async { if (bind.mainHasValid2FaSync()) { await bind.mainSetOption(key: "2fa", value: ""); + await bind.mainClearTrustedDevices(); callback?.call(); return; } @@ -1896,6 +1909,7 @@ void enter2FaDialog( SessionID sessionId, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); final RxBool submitReady = false.obs; + final RxBool trustThisDevice = false.obs; dialogManager.dismissAll(); dialogManager.show((setState, close, context) { @@ -1905,7 +1919,7 @@ void enter2FaDialog( } submit() { - gFFI.send2FA(sessionId, controller.text.trim()); + gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value); close(); dialogManager.showLoading(translate('Logging in...'), onCancel: closeConnection); @@ -1919,9 +1933,27 @@ void enter2FaDialog( onChanged: () => submitReady.value = codeField.isReady, ); + final trustField = Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text(translate("Trust this device")), + value: trustThisDevice.value, + onChanged: (value) { + if (value == null) return; + trustThisDevice.value = value; + }, + )); + return CustomAlertDialog( title: Text(translate('enter-2fa-title')), - content: codeField, + content: Column( + children: [ + codeField, + if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId)) + trustField, + ], + ), actions: [ dialogButton('Cancel', onPressed: cancel, @@ -2149,7 +2181,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) { }, ), ), - ), + ).workaroundFreezeLinuxMint(), if (!gFFI.abModel.current.isPersonal()) Row(children: [ Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), @@ -2216,3 +2248,255 @@ void CommonConfirmDialog(OverlayDialogManager dialogManager, String content, ); }); } + +void changeUnlockPinDialog(String oldPin, Function() callback) { + final pinController = TextEditingController(text: oldPin); + final confirmController = TextEditingController(text: oldPin); + String? pinErrorText; + String? confirmationErrorText; + final maxLength = bind.mainMaxEncryptLen(); + gFFI.dialogManager.show((setState, close, context) { + submit() async { + pinErrorText = null; + confirmationErrorText = null; + final pin = pinController.text.trim(); + final confirm = confirmController.text.trim(); + if (pin != confirm) { + setState(() { + confirmationErrorText = + translate('The confirmation is not identical.'); + }); + return; + } + final errorMsg = bind.mainSetUnlockPin(pin: pin); + if (errorMsg != '') { + setState(() { + pinErrorText = translate(errorMsg); + }); + return; + } + callback.call(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Set PIN")), + content: Column( + children: [ + DialogTextField( + title: 'PIN', + controller: pinController, + obscureText: true, + errorText: pinErrorText, + maxLength: maxLength, + ), + DialogTextField( + title: translate('Confirmation'), + controller: confirmController, + obscureText: true, + errorText: confirmationErrorText, + maxLength: maxLength, + ) + ], + ).marginOnly(bottom: 12), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void checkUnlockPinDialog(String correctPin, Function() passCallback) { + final controller = TextEditingController(); + String? errorText; + gFFI.dialogManager.show((setState, close, context) { + submit() async { + final pin = controller.text.trim(); + if (correctPin != pin) { + setState(() { + errorText = translate('Wrong PIN'); + }); + return; + } + passCallback.call(); + close(); + } + + return CustomAlertDialog( + content: Row( + children: [ + Expanded( + child: PasswordWidget( + title: 'PIN', + controller: controller, + errorText: errorText, + hintText: '', + )) + ], + ).marginOnly(bottom: 12), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void confrimDeleteTrustedDevicesDialog( + RxList trustedDevices, RxList selectedDevices) { + CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?', + () async { + if (selectedDevices.isEmpty) return; + if (selectedDevices.length == trustedDevices.length) { + await bind.mainClearTrustedDevices(); + trustedDevices.clear(); + selectedDevices.clear(); + } else { + final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList()); + await bind.mainRemoveTrustedDevices(json: json); + trustedDevices.removeWhere((element) { + return selectedDevices.contains(element.hwid); + }); + selectedDevices.clear(); + } + }); +} + +void manageTrustedDeviceDialog() async { + RxList trustedDevices = (await TrustedDevice.get()).obs; + RxList selectedDevices = RxList.empty(); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate("Manage trusted devices")), + content: trustedDevicesTable(trustedDevices, selectedDevices), + actions: [ + Obx(() => dialogButton(translate("Delete"), + onPressed: selectedDevices.isEmpty + ? null + : () { + confrimDeleteTrustedDevicesDialog( + trustedDevices, + selectedDevices, + ); + }, + isOutline: false) + .marginOnly(top: 12)), + dialogButton(translate("Close"), onPressed: close, isOutline: true) + .marginOnly(top: 12), + ], + onCancel: close, + ); + }); +} + +class TrustedDevice { + late final Uint8List hwid; + late final int time; + late final String id; + late final String name; + late final String platform; + + TrustedDevice.fromJson(Map json) { + final hwidList = json['hwid'] as List; + hwid = Uint8List.fromList(hwidList.cast()); + time = json['time']; + id = json['id']; + name = json['name']; + platform = json['platform']; + } + + String daysRemaining() { + final expiry = time + 90 * 24 * 60 * 60 * 1000; + final remaining = expiry - DateTime.now().millisecondsSinceEpoch; + if (remaining < 0) { + return '0'; + } + return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0); + } + + static Future> get() async { + final List devices = List.empty(growable: true); + try { + final devicesJson = await bind.mainGetTrustedDevices(); + if (devicesJson.isNotEmpty) { + final devicesList = json.decode(devicesJson); + if (devicesList is List) { + for (var device in devicesList) { + devices.add(TrustedDevice.fromJson(device)); + } + } + } + } catch (e) { + print(e.toString()); + } + devices.sort((a, b) => b.time.compareTo(a.time)); + return devices; + } +} + +Widget trustedDevicesTable( + RxList devices, RxList selectedDevices) { + RxBool selectAll = false.obs; + setSelectAll() { + if (selectedDevices.isNotEmpty && + selectedDevices.length == devices.length) { + selectAll.value = true; + } else { + selectAll.value = false; + } + } + + devices.listen((_) { + setSelectAll(); + }); + selectedDevices.listen((_) { + setSelectAll(); + }); + return FittedBox( + child: Obx(() => DataTable( + columns: [ + DataColumn( + label: Checkbox( + value: selectAll.value, + onChanged: (value) { + if (value == true) { + selectedDevices.clear(); + selectedDevices.addAll(devices.map((e) => e.hwid)); + } else { + selectedDevices.clear(); + } + }, + )), + DataColumn(label: Text(translate('Platform'))), + DataColumn(label: Text(translate('ID'))), + DataColumn(label: Text(translate('Username'))), + DataColumn(label: Text(translate('Days remaining'))), + ], + rows: devices.map((device) { + return DataRow(cells: [ + DataCell(Checkbox( + value: selectedDevices.contains(device.hwid), + onChanged: (value) { + if (value == null) return; + if (value) { + selectedDevices.remove(device.hwid); + selectedDevices.add(device.hwid); + } else { + selectedDevices.remove(device.hwid); + } + }, + )), + DataCell(Text(device.platform)), + DataCell(Text(device.id)), + DataCell(Text(device.name)), + DataCell(Text(device.daysRemaining())), + ]); + }).toList(), + )), + ); +} diff --git a/flutter/lib/common/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart index 6c269656722f..b3cfeae6e632 100644 --- a/flutter/lib/common/widgets/gestures.dart +++ b/flutter/lib/common/widgets/gestures.dart @@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { // end switch (_currentState) { case GestureState.oneFingerPan: - debugPrint("TwoFingerState.pan onEnd"); + debugPrint("OneFingerState.pan onEnd"); if (onOneFingerPanEnd != null) { onOneFingerPanEnd!(_getDragEndDetails(d)); } diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 71f3dacc3b2c..50e05cde5881 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -678,7 +678,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 e139ce700d22..359fbc7f7212 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -30,6 +31,8 @@ class _MyGroupState extends State { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { return const Center( child: CircularProgressIndicator(), @@ -43,15 +46,15 @@ class _MyGroupState extends State { retry: null, close: () => gFFI.groupModel.groupLoadError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildDesktop() - : _buildMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildPortrait() + : _buildLandscape())), ], ); }); } - Widget _buildDesktop() { + Widget _buildLandscape() { return Row( children: [ Container( @@ -80,14 +83,14 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); } - Widget _buildMobile() { + Widget _buildPortrait() { return Column( children: [ Container( @@ -112,8 +115,8 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); @@ -142,7 +145,7 @@ class _MyGroupState extends State { border: InputBorder.none, isDense: true, ), - )), + ).workaroundFreezeLinuxMint()), ], ); } @@ -157,14 +160,14 @@ class _MyGroupState extends State { } return true; }).toList(); - final listView = ListView.builder( - shrinkWrap: isMobile, + listView(bool isPortrait) => ListView.builder( + shrinkWrap: isPortrait, itemCount: items.length, itemBuilder: (context, index) => _buildUserItem(items[index])); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? listView - : LimitedBox(maxHeight: maxHeight, child: listView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? listView(false) + : LimitedBox(maxHeight: maxHeight, child: listView(true))); }); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f9dd73bbf6b8..b4bca12a9e67 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard> @override Widget build(BuildContext context) { super.build(context); - if (isDesktop || isWebDesktop) { - return _buildDesktop(); - } else { - return _buildMobile(); - } + return Obx(() => + stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape()); } - Widget _buildMobile() { - final peer = super.widget.peer; + Widget gestureDetector({required Widget child}) { final PeerTabModel peerTabModel = Provider.of(context); - return Card( - margin: EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - if (peerTabModel.multiSelectionMode) { - peerTabModel.select(peer); + final peer = super.widget.peer; + return GestureDetector( + onDoubleTap: peerTabModel.multiSelectionMode + ? null + : () => widget.connect(context, peer.id), + onTap: () { + if (peerTabModel.multiSelectionMode) { + peerTabModel.select(peer); + } else { + if (isMobile) { + widget.connect(context, peer.id); } else { - if (!isWebDesktop) { - connectInPeerTab(context, peer, widget.tab); - } + peerTabModel.select(peer); } - }, - onDoubleTap: isWebDesktop - ? () => connectInPeerTab(context, peer, widget.tab) - : null, - onLongPress: () { - peerTabModel.select(peer); - }, + } + }, + onLongPress: () => peerTabModel.select(peer), + child: child); + } + + Widget _buildPortrait() { + final peer = super.widget.peer; + return Card( + margin: EdgeInsets.symmetric(horizontal: 2), + child: gestureDetector( child: Container( padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), child: _buildPeerTile(context, peer, null)), )); } - Widget _buildDesktop() { - final PeerTabModel peerTabModel = Provider.of(context); + Widget _buildLandscape() { final peer = super.widget.peer; var deco = Rx( BoxDecoration( @@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard> ), ); }, - child: GestureDetector( - onDoubleTap: - peerTabModel.multiSelectionMode || peerTabModel.isShiftDown - ? null - : () => widget.connect(context, peer.id), - onTap: () => peerTabModel.select(peer), - onLongPress: () => peerTabModel.select(peer), + child: gestureDetector( child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), ); } - Widget _buildPeerTile( - BuildContext context, Peer peer, Rx? deco) { - hideUsernameOnCard ??= - bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; + makeChild(bool isPortrait, Peer peer) { final name = hideUsernameOnCard == true ? peer.hostname : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); - final child = Row( + return Row( mainAxisSize: MainAxisSize.max, children: [ Container( decoration: BoxDecoration( color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: isMobile + borderRadius: isPortrait ? BorderRadius.circular(_tileRadius) : BorderRadius.only( topLeft: Radius.circular(_tileRadius), @@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard> ), ), alignment: Alignment.center, - width: isMobile ? 50 : 42, - height: isMobile ? 50 : null, + width: isPortrait ? 50 : 42, + height: isPortrait ? 50 : null, child: Stack( children: [ - getPlatformImage(peer.platform, size: isMobile ? 38 : 30) + getPlatformImage(peer.platform, size: isPortrait ? 38 : 30) .paddingAll(6), if (_shouldBuildPasswordIcon(peer)) Positioned( @@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard> child: Column( children: [ Row(children: [ - getOnline(isMobile ? 4 : 8, peer.online), + getOnline(isPortrait ? 4 : 8, peer.online), Expanded( child: Text( peer.alias.isEmpty ? formatID(peer.id) : peer.alias, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, )), - ]).marginOnly(top: isMobile ? 0 : 2), + ]).marginOnly(top: isPortrait ? 0 : 2), Align( alignment: Alignment.centerLeft, child: Text( name, - style: isMobile ? null : greyStyle, + style: isPortrait ? null : greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, ), @@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard> ], ).marginOnly(top: 2), ), - isMobile - ? checkBoxOrActionMoreMobile(peer) - : checkBoxOrActionMoreDesktop(peer, isTile: true), + isPortrait + ? checkBoxOrActionMorePortrait(peer) + : checkBoxOrActionMoreLandscape(peer, isTile: true), ], ).paddingOnly(left: 10.0, top: 3.0), ), ) ], ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx? deco) { + hideUsernameOnCard ??= + bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; final colors = _frontN(peer.tags, 25) .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : peer.tags.isNotEmpty ? '${translate('Tags')}: ${peer.tags.join(', ')}' : '', child: Stack(children: [ - deco == null - ? child - : Obx( - () => Container( + Obx( + () => deco == null + ? makeChild(stateGlobal.isPortrait.isTrue, peer) + : Container( foregroundDecoration: deco.value, - child: child, + child: makeChild(stateGlobal.isPortrait.isTrue, peer), ), - ), + ), if (colors.isNotEmpty) - Positioned( - top: 2, - right: isMobile ? 20 : 10, - child: CustomPaint( - painter: TagPainter(radius: 3, colors: colors), - ), - ) + Obx(() => Positioned( + top: 2, + right: stateGlobal.isPortrait.isTrue ? 20 : 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + )) ]), ); } @@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard> color: Colors.transparent, elevation: 0, margin: EdgeInsets.zero, + // to-do: memory leak here, more investigation needed. + // Continious rebuilds of `Obx()` will cause memory leak here. + // The simple demo does not have this issue. child: Obx( () => Container( foregroundDecoration: deco.value, @@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard> style: Theme.of(context).textTheme.titleSmall, )), ]).paddingSymmetric(vertical: 8)), - checkBoxOrActionMoreDesktop(peer, isTile: false), + checkBoxOrActionMoreLandscape(peer, isTile: false), ], ).paddingSymmetric(horizontal: 12.0), ) @@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard> } } - Widget checkBoxOrActionMoreMobile(Peer peer) { + Widget checkBoxOrActionMorePortrait(Peer peer) { final PeerTabModel peerTabModel = Provider.of(context); final selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard> } } - Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { + Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) { final PeerTabModel peerTabModel = Provider.of(context); final selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard { } void _rdpDialog(String id) async { + final maxLength = bind.mainMaxEncryptLen(); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); @@ -1253,58 +1257,58 @@ void _rdpDialog(String id) async { hintText: '3389'), controller: portController, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: isDesktop ? 8 : 0), - Row( - children: [ - (isDesktop || isWebDesktop) - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.right, - ).marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: TextField( - decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) - ? null - : translate('Username')), - controller: userController, - ), - ), - ], - ).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0), - Row( - children: [ - (isDesktop || isWebDesktop) - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.right, - ).marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: Obx(() => TextField( - obscureText: secure.value, + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: TextField( decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) - ? null - : translate('Password'), - suffixIcon: IconButton( - onPressed: () => secure.value = !secure.value, - icon: Icon(secure.value - ? Icons.visibility_off - : Icons.visibility))), - controller: passwordController, - )), - ), - ], - ) + labelText: + isDesktop ? null : translate('Username')), + controller: userController, + ).workaroundFreezeLinuxMint(), + ), + ], + ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + maxLength: maxLength, + decoration: InputDecoration( + labelText: + isDesktop ? null : translate('Password'), + suffixIcon: IconButton( + onPressed: () => + secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordController, + ).workaroundFreezeLinuxMint()), + ), + ], + )) ], ), ), diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 8fe73144999f..9d21ec6cd71d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -107,33 +108,33 @@ class _PeerTabPageState extends State Widget build(BuildContext context) { final model = Provider.of(context); Widget selectionWrap(Widget widget) { - return model.multiSelectionMode ? createMultiSelectionBar() : widget; + return model.multiSelectionMode ? createMultiSelectionBar(model) : widget; } return Column( textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 32, - child: Container( - padding: (isDesktop || isWebDesktop) - ? null - : EdgeInsets.symmetric(horizontal: 2), - child: selectionWrap(Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: - visibleContextMenuListener(_createSwitchBar(context))), - if (isMobile) - ..._mobileRightActions(context) - else - ..._desktopRightActions(context) - ], - )), - ), - ).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), + Obx(() => SizedBox( + height: 32, + child: Container( + padding: stateGlobal.isPortrait.isTrue + ? EdgeInsets.symmetric(horizontal: 2) + : null, + child: selectionWrap(Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), + if (stateGlobal.isPortrait.isTrue) + ..._portraitRightActions(context) + else + ..._landscapeRightActions(context) + ], + )), + ), + ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)), _createPeersView(), ], ); @@ -299,7 +300,7 @@ class _PeerTabPageState extends State } Widget visibleContextMenuListener(Widget child) { - if (isMobile) { + if (!(isDesktop || isWebDesktop)) { return GestureDetector( onLongPressDown: (e) { final x = e.globalPosition.dx; @@ -361,8 +362,7 @@ class _PeerTabPageState extends State .toList()); } - Widget createMultiSelectionBar() { - final model = Provider.of(context); + Widget createMultiSelectionBar(PeerTabModel model) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -380,7 +380,7 @@ class _PeerTabPageState extends State Row( children: [ selectionCount(model.selectedPeers.length), - selectAll(), + selectAll(model), closeSelection(), ], ) @@ -456,7 +456,7 @@ class _PeerTabPageState extends State showToast(translate('Successful')); }, child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -477,7 +477,7 @@ class _PeerTabPageState extends State model.setMultiSelectionMode(false); }, child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -500,7 +500,7 @@ class _PeerTabPageState extends State }); }, child: Icon(Icons.tag)) - .marginOnly(left: isMobile ? 11 : 6), + .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -511,8 +511,7 @@ class _PeerTabPageState extends State ); } - Widget selectAll() { - final model = Provider.of(context); + Widget selectAll(PeerTabModel model) { return Offstage( offstage: model.selectedPeers.length >= model.currentTabCachedPeers.length, @@ -556,10 +555,10 @@ class _PeerTabPageState extends State }); } - List _desktopRightActions(BuildContext context) { + List _landscapeRightActions(BuildContext context) { final model = Provider.of(context); return [ - const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), + const PeerSearchBar().marginOnly(right: 13), _createRefresh( index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), _createRefresh( @@ -580,7 +579,7 @@ class _PeerTabPageState extends State ]; } - List _mobileRightActions(BuildContext context) { + List _portraitRightActions(BuildContext context) { final model = Provider.of(context); final screenWidth = MediaQuery.of(context).size.width; final leftIconSize = Theme.of(context).iconTheme.size ?? 24; @@ -701,13 +700,13 @@ class _PeerSearchBarState extends State { baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); }); - return Container( - width: isMobile ? 120 : 140, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(6), - ), - child: Obx(() => Row( + return Obx(() => Container( + width: stateGlobal.isPortrait.isTrue ? 120 : 140, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + child: Row( children: [ Expanded( child: Row( @@ -744,7 +743,7 @@ class _PeerSearchBarState extends State { border: InputBorder.none, isDense: true, ), - ), + ).workaroundFreezeLinuxMint(), ), // Icon(Icons.close), IconButton( @@ -768,8 +767,8 @@ class _PeerSearchBarState extends State { ), ) ], - )), - ); + ), + )); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index ef9647eaa93d..3e34f882d1de 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -5,7 +5,9 @@ import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -41,6 +43,14 @@ class LoadEvent { static const String group = 'load_group_peers'; } +class PeersModelName { + static const String recent = 'recent peer'; + static const String favorite = 'fav peer'; + static const String lan = 'discovered peer'; + static const String addressBook = 'address book peer'; + static const String group = 'group peer'; +} + /// for peer search text, global obs value final peerSearchText = "".obs; @@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView> var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now(); + var _lastWindowRestoreTime = DateTime.now(); var _queryCount = 0; var _exit = false; bool _isActive = true; @@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView> @override void onWindowFocus() { _queryCount = 0; + _isActive = true; } @override - void onWindowMinimize() { + void onWindowBlur() { + // We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`. + // Maybe it's a bug of the window manager, but the source code seems to be correct. + // + // Although `onWindowRestore()` is called after `onWindowBlur()` in my test, + // we need the following comparison to ensure that `_isActive` is true in the end. + if (isWindows && + DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { + return; + } _queryCount = _maxQueryCount; + _isActive = false; + } + + @override + void onWindowRestore() { + // Window restore (on MacOS and Linux) also triggers `onWindowFocus()`. + // But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager. + if (!isWindows) return; + _queryCount = 0; + _isActive = true; + _lastWindowRestoreTime = DateTime.now(); + } + + @override + void onWindowMinimize() { + // Window minimize also triggers `onWindowBlur()`. } // This function is required for mobile. @@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView> @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - if (isDesktop) return; + if (isDesktop || isWebDesktop) return; if (state == AppLifecycleState.resumed) { _isActive = true; _queryCount = 0; @@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView> @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => widget.peers, + // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. + // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. + // Simple demo can reproduce this issue. + return ChangeNotifierProvider.value( + value: widget.peers, child: Consumer(builder: (context, peers, child) { if (peers.peers.isEmpty) { gFFI.peerTabModel.setCurrentTabCachedPeers([]); @@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView> var peers = snapshot.data!; if (peers.length > 1000) peers = peers.sublist(0, 1000); gFFI.peerTabModel.setCurrentTabCachedPeers(peers); - buildOnePeer(Peer peer) { + buildOnePeer(Peer peer, bool isPortrait) { final visibilityChild = VisibilityDetector( key: ValueKey(_cardId(peer.id)), onVisibilityChanged: onVisibilityChanged, @@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView> // No need to listen the currentTab change event. // Because the currentTab change event will trigger the peers change event, // and the peers change event will trigger _buildPeersView(). - return (isDesktop || isWebDesktop) + return !isPortrait ? Obx(() => peerCardUiType.value == PeerUiType.list ? Container(height: 45, child: visibilityChild) : peerCardUiType.value == PeerUiType.grid @@ -217,44 +258,36 @@ class _PeersViewState extends State<_PeersView> : Container(child: visibilityChild); } - final Widget child; - if (isMobile) { - child = ListView.builder( - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]).marginOnly( - top: index == 0 ? 0 : space / 2, bottom: space / 2); - }, - ); - } else { - child = Obx(() => peerCardUiType.value == PeerUiType.list - ? DesktopScrollWrapper( - scrollController: _scrollController, - child: ListView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]).marginOnly( - right: space, - top: index == 0 ? 0 : space / 2, - bottom: space / 2); - }), - ) - : DesktopScrollWrapper( - scrollController: _scrollController, - child: DynamicGridView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithWrapping( - mainAxisSpacing: space / 2, - crossAxisSpacing: space), - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]); - }), - )); - } + // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6. + // Continious rebuilds of `ListView.builder` will cause memory leak. + // Simple demo can reproduce this issue. + final Widget child = Obx(() => stateGlobal.isPortrait.isTrue + ? ListView.builder( + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], true).marginOnly( + top: index == 0 ? 0 : space / 2, bottom: space / 2); + }, + ) + : peerCardUiType.value == PeerUiType.list + ? ListView.builder( + controller: _scrollController, + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false).marginOnly( + right: space, + top: index == 0 ? 0 : space / 2, + bottom: space / 2); + }, + ) + : DynamicGridView.builder( + gridDelegate: SliverGridDelegateWithWrapping( + mainAxisSpacing: space / 2, + crossAxisSpacing: space), + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false); + })); if (updateEvent == UpdateEvent.load) { _curPeers.clear(); @@ -290,7 +323,12 @@ class _PeersViewState extends State<_PeersView> _queryOnlines(false); } } else { - if (_isActive && (_queryCount < _maxQueryCount || !p)) { + final skipIfIsWeb = + isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage); + final skipIfMobile = + (isAndroid || isIOS) && !stateGlobal.isInMainPage; + final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive; + if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) { if (now.difference(_lastQueryTime) >= _queryInterval) { if (_curPeers.isNotEmpty) { bind.queryOnlines(ids: _curPeers.toList(growable: false)); @@ -371,28 +409,39 @@ class _PeersViewState extends State<_PeersView> } abstract class BasePeersView extends StatelessWidget { - final String name; - final String loadEvent; + final PeerTabIndex peerTabIndex; final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; - final GetInitPeers? getInitPeers; const BasePeersView({ Key? key, - required this.name, - required this.loadEvent, + required this.peerTabIndex, this.peerFilter, required this.peerCardBuilder, - required this.getInitPeers, }) : super(key: key); @override Widget build(BuildContext context) { + Peers peers; + switch (peerTabIndex) { + case PeerTabIndex.recent: + peers = gFFI.recentPeersModel; + break; + case PeerTabIndex.fav: + peers = gFFI.favoritePeersModel; + break; + case PeerTabIndex.lan: + peers = gFFI.lanPeersModel; + break; + case PeerTabIndex.ab: + peers = gFFI.abModel.peersModel; + break; + case PeerTabIndex.group: + peers = gFFI.groupModel.peersModel; + break; + } return _PeersView( - peers: - Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers), - peerFilter: peerFilter, - peerCardBuilder: peerCardBuilder); + peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } } @@ -401,13 +450,11 @@ class RecentPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'recent peer', - loadEvent: LoadEvent.recent, + peerTabIndex: PeerTabIndex.recent, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -423,13 +470,11 @@ class FavoritePeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'favorite peer', - loadEvent: LoadEvent.favorite, + peerTabIndex: PeerTabIndex.fav, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -445,13 +490,11 @@ class DiscoveredPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'discovered peer', - loadEvent: LoadEvent.lan, + peerTabIndex: PeerTabIndex.lan, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -464,36 +507,38 @@ class DiscoveredPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView { AddressBookPeersView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'address book peer', - loadEvent: LoadEvent.addressBook, + peerTabIndex: PeerTabIndex.ab, peerFilter: (Peer peer) => _hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool _hitTag(List selectedTags, List idents) { if (selectedTags.isEmpty) { return true; } + // The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations. + final selectedNormalTags = + selectedTags.where((tag) => tag != kUntagged).toList(); + if (selectedTags.contains(kUntagged)) { + if (idents.isEmpty) return true; + if (selectedNormalTags.isEmpty) return false; + } if (gFFI.abModel.filterByIntersection.value) { - for (final tag in selectedTags) { + for (final tag in selectedNormalTags) { if (!idents.contains(tag)) { return false; } } return true; } else { - for (final tag in selectedTags) { + for (final tag in selectedNormalTags) { if (idents.contains(tag)) { return true; } @@ -505,20 +550,15 @@ class AddressBookPeersView extends BasePeersView { class MyGroupPeerView extends BasePeersView { MyGroupPeerView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'group peer', - loadEvent: LoadEvent.group, + peerTabIndex: PeerTabIndex.group, peerFilter: filter, peerCardBuilder: (Peer peer) => MyGroupPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool filter(Peer peer) { diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 61bd4dd31bc5..6eb9b0594708 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { + // https://github.com/flutter/flutter/issues/154053 + final useRawKeyEvents = isLinux && !isWeb; + // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events, + // while `Alt` and `Control` are seperated key events for en-US input method. return FocusScope( autofocus: true, child: Focus( @@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: (FocusNode data, RawKeyEvent e) => - inputModel.handleRawKeyEvent(e), + onKey: useRawKeyEvents + ? (FocusNode data, RawKeyEvent event) => + inputModel.handleRawKeyEvent(event) + : null, + onKeyEvent: useRawKeyEvents + ? null + : (FocusNode node, KeyEvent event) => + inputModel.handleKeyEvent(event), child: child)); } } @@ -74,8 +84,17 @@ class _RawTouchGestureDetectorRegionState double _mouseScrollIntegral = 0; // mouse scroll speed controller double _scale = 1; + // Workaround tap down event when two fingers are used to scale(mobile) + TapDownDetails? _lastTapDownDetails; + PointerDeviceKind? lastDeviceKind; + // For touch mode, onDoubleTap + // `onDoubleTap()` does not provide the position of the tap event. + Offset _lastPosOfDoubleTapDown = Offset.zero; + bool _touchModePanStarted = false; + Offset _doubleFinerTapPosition = Offset.zero; + FFI get ffi => widget.ffi; FfiModel get ffiModel => widget.ffiModel; InputModel get inputModel => widget.inputModel; @@ -90,152 +109,190 @@ class _RawTouchGestureDetectorRegionState ); } - onTapDown(TapDownDetails d) { + onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; // Desktop or mobile "Touch mode" - if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { - inputModel.tapDown(MouseButtons.left); - } + _lastTapDownDetails = d; } } - onTapUp(TapUpDetails d) { + onTapUp(TapUpDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { - inputModel.tapUp(MouseButtons.left); + final isMoved = + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + if (isMoved) { + if (lastTapDownDetails != null) { + await inputModel.tapDown(MouseButtons.left); + } + await inputModel.tapUp(MouseButtons.left); } } } - onTap() { + onTap() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { // Mobile, "Mouse mode" - inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); } } - onDoubleTapDown(TapDownDetails d) { + onDoubleTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _lastPosOfDoubleTapDown = d.localPosition; + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } } - onDoubleTap() { + onDoubleTap() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { return; } - inputModel.tap(MouseButtons.left); - inputModel.tap(MouseButtons.left); + if (handleTouch && + !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) { + return; + } + await inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); } - onLongPressDown(LongPressDownDetails d) { + onLongPressDown(LongPressDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _lastPosOfDoubleTapDown = d.localPosition; _cacheLongPressPosition = d.localPosition; + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; } } - onLongPressUp() { + onLongPressUp() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - inputModel.tapUp(MouseButtons.left); + await inputModel.tapUp(MouseButtons.left); } } // for mobiles - onLongPress() { + onLongPress() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - ffi.cursorModel + final isMoved = await ffi.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!isMoved) { + return; + } + } + if (!ffi.ffiModel.isPeerMobile) { + await inputModel.tap(MouseButtons.right); } - inputModel.tap(MouseButtons.right); } - onDoubleFinerTapDown(TapDownDetails d) { + onDoubleFinerTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } + _doubleFinerTapPosition = d.localPosition; // ignore for desktop and mobile } - onDoubleFinerTap(TapDownDetails d) { + onDoubleFinerTap(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } - if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) { - inputModel.tap(MouseButtons.right); + + // mobile mouse mode or desktop touch screen + final isMobileMouseMode = isMobile && !ffiModel.touchMode; + // We can't use `d.localPosition` here because it's always (0, 0) on desktop. + final isDesktopInRemoteRect = (isDesktop || isWebDesktop) && + ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition); + if (isMobileMouseMode || isDesktopInRemoteRect) { + await inputModel.tap(MouseButtons.right); } } - onHoldDragStart(DragStartDetails d) { + onHoldDragStart(DragStartDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - inputModel.sendMouse('down', MouseButtons.left); + await inputModel.sendMouse('down', MouseButtons.left); } } - onHoldDragUpdate(DragUpdateDetails d) { + onHoldDragUpdate(DragUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } } - onHoldDragEnd(DragEndDetails d) { + onHoldDragEnd(DragEndDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } } - onOneFingerPanStart(BuildContext context, DragStartDetails d) { + onOneFingerPanStart(BuildContext context, DragStartDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; lastDeviceKind = d.kind ?? lastDeviceKind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { + if (lastTapDownDetails != null) { + await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx, + lastTapDownDetails.localPosition.dy); + } if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } - if (isDesktop) { + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } + + _touchModePanStarted = true; + if (isDesktop || isWebDesktop) { ffi.cursorModel.trySetRemoteWindowCoords(); } + // Workaround for the issue that the first pan event is sent a long time after the start event. // If the time interval between the start event and the first pan event is less than 500ms, // we consider to use the long press position as the start position. @@ -243,11 +300,11 @@ class _RawTouchGestureDetectorRegionState // TODO: We should find a better way to send the first pan event as soon as possible. if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs < 500) { - ffi.cursorModel + await ffi.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - inputModel.sendMouse('down', MouseButtons.left); - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + await inputModel.sendMouse('down', MouseButtons.left); + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } else { final offset = ffi.cursorModel.offset; final cursorX = offset.dx; @@ -256,39 +313,46 @@ class _RawTouchGestureDetectorRegionState ffi.cursorModel.getVisibleRect().inflate(1); // extend edges final size = MediaQueryData.fromView(View.of(context)).size; if (!visible.contains(Offset(cursorX, cursorY))) { - ffi.cursorModel.move(size.width / 2, size.height / 2); + await ffi.cursorModel.move(size.width / 2, size.height / 2); } } } - onOneFingerPanUpdate(DragUpdateDetails d) { + onOneFingerPanUpdate(DragUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } - ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + if (handleTouch && !_touchModePanStarted) { + return; + } + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } - onOneFingerPanEnd(DragEndDetails d) { + onOneFingerPanEnd(DragEndDetails d) async { + _touchModePanStarted = false; if (lastDeviceKind != PointerDeviceKind.touch) { return; } - if (isDesktop) { + if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } - 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) { return; } } - onTwoFingerScaleUpdate(ScaleUpdateDetails d) { + onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } @@ -297,7 +361,7 @@ class _RawTouchGestureDetectorRegionState _scale = d.scale; if (scale != 0) { - bind.sessionSendPointer( + await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( PointerEventToRust(kPointerEventKindTouch, 'scale', scale) @@ -312,21 +376,22 @@ class _RawTouchGestureDetectorRegionState } } - onTwoFingerScaleEnd(ScaleEndDetails d) { + onTwoFingerScaleEnd(ScaleEndDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if ((isDesktop || isWebDesktop)) { - bind.sessionSendPointer( + await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson())); } else { // mobile _scale = 1; - bind.sessionSetViewStyle(sessionId: sessionId, value: ""); + // No idea why we need to set the view style to "" here. + // bind.sessionSetViewStyle(sessionId: sessionId, value: ""); } - inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } get onHoldDragCancel => null; diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0b56d9f4c149..153121057e5e 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -147,12 +147,23 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } + + connectWithToken( + {required bool isFileTransfer, required bool isTcpTunneling}) { + final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); + connect(context, id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + connToken: connToken); + } + // transferFile if (isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => connect(context, id, isFileTransfer: true)), + onPressed: () => + connectWithToken(isFileTransfer: true, isTcpTunneling: false)), ); } // tcpTunneling @@ -160,7 +171,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => connect(context, id, isTcpTunneling: true)), + onPressed: () => + connectWithToken(isFileTransfer: false, isTcpTunneling: true)), ); } // note @@ -183,7 +195,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( TTextMenu( - child: Text('${translate("Insert")} Ctrl + Alt + Del'), + child: Text('${translate("Insert Ctrl + Alt + Del")}'), onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), ); } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 5af3f6d251fd..95b207826a71 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; +const String kPeerPlatformWebDesktop = "WebDesktop"; const double kScrollbarThickness = 12.0; @@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect"; const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout"; const String kOptionEnableHwcodec = "enable-hwcodec"; const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming"; +const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing"; const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; @@ -136,6 +138,7 @@ 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 kOptionEnableTrustedDevices = "enable-trusted-devices"; // buildin opitons const String kOptionHideServerSetting = "hide-server-settings"; @@ -166,6 +169,13 @@ const int kWindowMainId = 0; const String kPointerEventKindTouch = "touch"; const String kPointerEventKindMouse = "mouse"; +const String kMouseEventTypeDefault = ""; +const String kMouseEventTypePanStart = "pan_start"; +const String kMouseEventTypePanUpdate = "pan_update"; +const String kMouseEventTypePanEnd = "pan_end"; +const String kMouseEventTypeDown = "down"; +const String kMouseEventTypeUp = "up"; + const String kKeyFlutterKey = "flutter_key"; const String kKeyShowDisplaysAsIndividualWindows = @@ -234,15 +244,11 @@ const double kDesktopIconButtonSplashRadius = 20; /// [kMinCursorSize] indicates min cursor (w, h) const int kMinCursorSize = 12; -/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse -const kDefaultScrollAmountMultiplier = 5.0; -const kDefaultScrollDuration = Duration(milliseconds: 50); -const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; -// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead. -final kWindowEdgeSize = isWindows ? 1.0 : 5.0; -final kWindowBorderWidth = 1.0; +// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. +const kWindowResizeEdgeSize = 5.0; +const kWindowBorderWidth = 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kFrameBorderRadius = 12.0; const kFrameClipRRectBorderRadius = 12.0; @@ -568,3 +574,5 @@ enum WindowsTarget { extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } + +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1403d4493471..235e2185a063 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:auto_size_text/auto_size_text.dart'; 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/models/state_model.dart'; import 'package:get/get.dart'; @@ -39,7 +39,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); @@ -169,20 +169,19 @@ class _OnlineStatusWidgetState extends State { final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; final statusNum = status['status_num'] as int; - final preStatus = stateGlobal.svcStatus.value; if (statusNum == 0) { stateGlobal.svcStatus.value = SvcStatus.connecting; } else if (statusNum == -1) { stateGlobal.svcStatus.value = SvcStatus.notReady; } else if (statusNum == 1) { stateGlobal.svcStatus.value = SvcStatus.ready; - if (preStatus != SvcStatus.ready) { - gFFI.userModel.refreshCurrentUser(); - } } else { stateGlobal.svcStatus.value = SvcStatus.notReady; } _svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); + try { + stateGlobal.videoConnCount.value = status['video_conn_count'] as int; + } catch (_) {} } } @@ -207,6 +206,8 @@ class _ConnectionPageState extends State bool isPeersLoading = false; bool isPeersLoaded = false; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; @override void initState() { @@ -261,8 +262,9 @@ class _ConnectionPageState extends State @override void onWindowLeaveFullScreen() { // Restore edge border to default edge size. - stateGlobal.resizeEdgeSize.value = - stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize; + stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue + ? kMaximizeEdgeSize + : windowResizeEdgeSize; } @override @@ -326,43 +328,14 @@ class _ConnectionPageState extends State child: Ink( child: Column( children: [ - Row( - children: [ - Expanded( - child: Row( - children: [ - AutoSizeText( - translate('Control Remote Desktop'), - maxLines: 1, - style: Theme.of(context) - .textTheme - .titleLarge - ?.merge(TextStyle(height: 1)), - ).marginOnly(right: 4), - Tooltip( - waitDuration: Duration(milliseconds: 300), - message: translate("id_input_tip"), - child: Icon( - Icons.help_outline_outlined, - size: 16, - color: Theme.of(context) - .textTheme - .titleLarge - ?.color - ?.withOpacity(0.5), - ), - ), - ], - )), - ], - ).marginOnly(bottom: 15), + getConnectionPageTitle(context, false).marginOnly(bottom: 15), Row( children: [ Expanded( child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -378,7 +351,7 @@ class _ConnectionPageState extends State rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -389,8 +362,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -402,6 +374,7 @@ class _ConnectionPageState extends State peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: ( BuildContext context, @@ -451,7 +424,7 @@ class _ConnectionPageState extends State onSubmitted: (_) { onConnect(); }, - )); + ).workaroundFreezeLinuxMint()); }, onSelected: (option) { setState(() { @@ -462,6 +435,7 @@ class _ConnectionPageState extends State optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 31a8e1374ff5..ba724eed5eee 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,9 +12,9 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -35,12 +35,11 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final _leftPaneScrollController = ScrollController(); @override bool get wantKeepAlive => true; - var updateUrl = ''; var systemError = ''; StreamSubscription? _uniLinksSubscription; var svcStopped = false.obs; @@ -52,6 +51,7 @@ class _DesktopHomePageState extends State bool isCardClosed = false; final RxBool _editHover = false.obs; + final RxBool _block = false.obs; final GlobalKey _childKey = GlobalKey(); @@ -59,14 +59,20 @@ class _DesktopHomePageState extends State Widget build(BuildContext context) { super.build(context); final isIncomingOnly = bind.isIncomingOnly(); - return Row( + return _buildBlock( + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ buildLeftPane(context), if (!isIncomingOnly) const VerticalDivider(width: 1), if (!isIncomingOnly) Expanded(child: buildRightPane(context)), ], - ); + )); + } + + Widget _buildBlock({required Widget child}) { + return buildRemoteBlock( + block: _block, mask: true, use: canBeBlocked, child: child); } Widget buildLeftPane(BuildContext context) { @@ -87,7 +93,8 @@ class _DesktopHomePageState extends State if (!isOutgoingOnly) buildIDBoard(context), if (!isOutgoingOnly) buildPasswordBoard(context), FutureBuilder( - future: buildHelpCards(), + future: Future.value( + Obx(() => buildHelpCards(stateGlobal.updateUrl.value))), builder: (_, data) { if (data.hasData) { if (isIncomingOnly) { @@ -125,47 +132,43 @@ class _DesktopHomePageState extends State child: Container( width: isIncomingOnly ? 280.0 : 200.0, color: Theme.of(context).colorScheme.background, - child: DesktopScrollWrapper( - scrollController: _leftPaneScrollController, - child: Stack( - children: [ - SingleChildScrollView( - controller: _leftPaneScrollController, - physics: DraggableNeverScrollableScrollPhysics(), - child: Column( - key: _childKey, - children: children, - ), + child: Stack( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, ), - if (isOutgoingOnly) - Positioned( - bottom: 6, - left: 12, - child: Align( - alignment: Alignment.centerLeft, - child: InkWell( - child: Obx( - () => Icon( - Icons.settings, - color: _editHover.value - ? textColor - : Colors.grey.withOpacity(0.5), - size: 22, - ), + ), + if (isOutgoingOnly) + Positioned( + bottom: 6, + left: 12, + child: Align( + alignment: Alignment.centerLeft, + child: InkWell( + child: Obx( + () => Icon( + Icons.settings, + color: _editHover.value + ? textColor + : Colors.grey.withOpacity(0.5), + size: 22, ), - onTap: () => { - if (DesktopSettingPage.tabKeys.isNotEmpty) - { - DesktopSettingPage.switch2page( - DesktopSettingPage.tabKeys[0]) - } - }, - onHover: (value) => _editHover.value = value, ), + onTap: () => { + if (DesktopSettingPage.tabKeys.isNotEmpty) + { + DesktopSettingPage.switch2page( + DesktopSettingPage.tabKeys[0]) + } + }, + onHover: (value) => _editHover.value = value, ), - ) - ], - ), + ), + ) + ], ), ), ); @@ -234,7 +237,7 @@ class _DesktopHomePageState extends State style: TextStyle( fontSize: 22, ), - ), + ).workaroundFreezeLinuxMint(), ), ) ], @@ -272,10 +275,21 @@ class _DesktopHomePageState extends State } buildPasswordBoard(BuildContext context) { - final model = gFFI.serverModel; + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer( + builder: (context, model, child) { + return buildPasswordBoard2(context, model); + }, + )); + } + + buildPasswordBoard2(BuildContext context, ServerModel model) { RxBool refreshHover = false.obs; RxBool editHover = false.obs; final textColor = Theme.of(context).textTheme.titleLarge?.color; + final showOneTime = model.approveMode != 'click' && + model.verificationMethod != kUsePermanentPassword; return Container( margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( @@ -304,8 +318,7 @@ class _DesktopHomePageState extends State Expanded( child: GestureDetector( onDoubleTap: () { - if (model.verificationMethod != - kUsePermanentPassword) { + if (showOneTime) { Clipboard.setData( ClipboardData(text: model.serverPasswd.text)); showToast(translate("Copied")); @@ -320,25 +333,26 @@ class _DesktopHomePageState extends State EdgeInsets.only(top: 14, bottom: 10), ), style: TextStyle(fontSize: 15), - ), + ).workaroundFreezeLinuxMint(), ), ), - AnimatedRotationWidget( - onPressed: () => bind.mainUpdateTemporaryPassword(), - child: Tooltip( - message: translate('Refresh Password'), - child: Obx(() => RotatedBox( - quarterTurns: 2, - child: Icon( - Icons.refresh, - color: refreshHover.value - ? textColor - : Color(0xFFDDDDDD), - size: 22, - ))), - ), - onHover: (value) => refreshHover.value = value, - ).marginOnly(right: 8, top: 4), + if (showOneTime) + AnimatedRotationWidget( + onPressed: () => bind.mainUpdateTemporaryPassword(), + child: Tooltip( + message: translate('Refresh Password'), + child: Obx(() => RotatedBox( + quarterTurns: 2, + child: Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ))), + ), + onHover: (value) => refreshHover.value = value, + ).marginOnly(right: 8, top: 4), if (!bind.isDisableSettings()) InkWell( child: Tooltip( @@ -409,14 +423,14 @@ class _DesktopHomePageState extends State ); } - Future buildHelpCards() async { + Widget buildHelpCards(String updateUrl) { if (!bind.isCustomClient() && updateUrl.isNotEmpty && !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { return buildInstallCard( "Status", - "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", + "${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); @@ -663,12 +677,6 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); - if (!bind.isCustomClient()) { - Timer(const Duration(seconds: 1), () async { - updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (updateUrl.isNotEmpty) setState(() {}); - }); - } _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { await gFFI.serverModel.fetchID(); final error = await bind.mainGetError(); @@ -766,6 +774,7 @@ class _DesktopHomePageState extends State isRDP: call.arguments['isRDP'], password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], + connToken: call.arguments['connToken'], ); } else if (call.method == kWindowEventMoveTabToNewWindow) { final args = call.arguments.split(','); @@ -803,6 +812,7 @@ class _DesktopHomePageState extends State _updateWindowSize(); }); } + WidgetsBinding.instance.addObserver(this); } _updateWindowSize() { @@ -824,9 +834,18 @@ class _DesktopHomePageState extends State _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + Widget buildPluginEntry() { final entries = PluginUiManager.instance.entries.entries; return Offstage( @@ -857,6 +876,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { // SpecialCharacterValidationRule(), MinCharactersValidationRule(8), ]; + final maxLength = bind.mainMaxEncryptLen(); gFFI.dialogManager.show((setState, close, context) { submit() { @@ -915,7 +935,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg0 = ''; }); }, - ), + maxLength: maxLength, + ).workaroundFreezeLinuxMint(), ), ], ), @@ -941,7 +962,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg1 = ''; }); }, - ), + maxLength: maxLength, + ).workaroundFreezeLinuxMint(), ), ], ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 79c18c521183..f89381a3ff5b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,15 +11,16 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; @@ -61,7 +62,8 @@ class DesktopSettingPage extends StatefulWidget { final SettingsTabKey initialTabkey; static final List tabKeys = [ SettingsTabKey.general, - if (!bind.isOutgoingOnly() && + if (!isWeb && + !bind.isOutgoingOnly() && !bind.isDisableSettings() && bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') SettingsTabKey.safety, @@ -105,13 +107,20 @@ class DesktopSettingPage extends StatefulWidget { } class _DesktopSettingPageState extends State - with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + with + TickerProviderStateMixin, + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver { late PageController controller; late Rx selectedTab; @override bool get wantKeepAlive => true; + final RxBool _block = false.obs; + final RxBool _canBeBlocked = false.obs; + Timer? _videoConnTimer; + _DesktopSettingPageState(SettingsTabKey initialTabkey) { var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey); if (initialIndex == -1) { @@ -131,11 +140,34 @@ class _DesktopSettingPageState extends State }); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _videoConnTimer = + periodic_immediate(Duration(milliseconds: 1000), () async { + if (!mounted) { + return; + } + _canBeBlocked.value = await canBeBlocked(); + }); + } + @override void dispose() { super.dispose(); Get.delete(tag: _kSettingPageControllerTag); Get.delete(tag: _kSettingPageTabKeyTag); + WidgetsBinding.instance.removeObserver(this); + _videoConnTimer?.cancel(); } List<_TabInfo> _settingTabs() { @@ -205,18 +237,41 @@ class _DesktopSettingPageState extends State return children; } + Widget _buildBlock({required List children}) { + // check both mouseMoveTime and videoConnCount + return Obx(() { + final videoConnBlock = + _canBeBlocked.value && stateGlobal.videoConnCount > 0; + return Stack(children: [ + buildRemoteBlock( + block: _block, + mask: false, + use: canBeBlocked, + child: preventMouseKeyBuilder( + child: Row(children: children), + block: videoConnBlock, + ), + ), + if (videoConnBlock) + Container( + color: Colors.black.withOpacity(0.5), + ) + ]); + }); + } + @override Widget build(BuildContext context) { super.build(context); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, - body: Row( + body: _buildBlock( children: [ SizedBox( width: _kTabWidth, child: Column( children: [ - _header(), + _header(context), Flexible(child: _listView(tabs: _settingTabs())), ], ), @@ -225,13 +280,11 @@ class _DesktopSettingPageState extends State Expanded( child: Container( color: Theme.of(context).scaffoldBackgroundColor, - child: DesktopScrollWrapper( - scrollController: controller, - child: PageView( - controller: controller, - physics: NeverScrollableScrollPhysics(), - children: _children(), - )), + child: PageView( + controller: controller, + physics: NeverScrollableScrollPhysics(), + children: _children(), + ), ), ) ], @@ -239,21 +292,40 @@ class _DesktopSettingPageState extends State ); } - Widget _header() { + Widget _header(BuildContext context) { + final settingsText = Text( + translate('Settings'), + textAlign: TextAlign.left, + style: const TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ); return Row( children: [ - SizedBox( - height: 62, - child: Text( - translate('Settings'), - textAlign: TextAlign.left, - style: const TextStyle( - color: _accentColor, - fontSize: _kTitleFontSize, - fontWeight: FontWeight.w400, + if (isWeb) + IconButton( + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + icon: Icon(Icons.arrow_back), + ).marginOnly(left: 5), + if (isWeb) + SizedBox( + height: 62, + child: Align( + alignment: Alignment.center, + child: settingsText, ), - ), - ).marginOnly(left: 20, top: 10), + ).marginOnly(left: 20), + if (!isWeb) + SizedBox( + height: 62, + child: settingsText, + ).marginOnly(left: 20, top: 10), const Spacer(), ], ); @@ -261,13 +333,10 @@ class _DesktopSettingPageState extends State Widget _listView({required List<_TabInfo> tabs}) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: tabs.map((tab) => _listItem(tab: tab)).toList(), - )); + return ListView( + controller: scrollController, + children: tabs.map((tab) => _listItem(tab: tab)).toList(), + ); } Widget _listItem({required _TabInfo tab}) { @@ -322,34 +391,32 @@ class _General extends StatefulWidget { } class _GeneralState extends State<_General> { - final RxBool serviceStop = Get.find(tag: 'stop-service'); + final RxBool serviceStop = + isWeb ? RxBool(false) : Get.find(tag: 'stop-service'); RxBool serviceBtnEnabled = true.obs; @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: [ - service(), - theme(), - _Card(title: 'Language', children: [language()]), - hwcodec(), - audio(context), - record(context), - WaylandCard(), - other() - ], - ).marginOnly(bottom: _kListViewBottomMargin)); + return ListView( + controller: scrollController, + children: [ + if (!isWeb) service(), + theme(), + _Card(title: 'Language', children: [language()]), + if (!isWeb) hwcodec(), + if (!isWeb) audio(context), + if (!isWeb) record(context), + if (!isWeb) WaylandCard(), + other() + ], + ).marginOnly(bottom: _kListViewBottomMargin); } Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); - onChanged(String value) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); + onChanged(String value) async { + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); setState(() {}); } @@ -394,13 +461,13 @@ class _GeneralState extends State<_General> { Widget other() { final children = [ - if (!bind.isIncomingOnly()) + if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), - wallpaper(), - if (!bind.isIncomingOnly()) ...[ + if (!isWeb) wallpaper(), + if (!isWeb && !bind.isIncomingOnly()) ...[ _OptionCheckBox( context, 'Open connection in new tab', @@ -417,18 +484,19 @@ class _GeneralState extends State<_General> { kOptionAllowAlwaysSoftwareRender, ), ), - Tooltip( - message: translate('texture_render_tip'), - child: _OptionCheckBox( - context, - "Use texture rendering", - kOptionTextureRender, - optGetter: bind.mainGetUseTextureRender, - optSetter: (k, v) async => - await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + if (!isWeb) + Tooltip( + message: translate('texture_render_tip'), + child: _OptionCheckBox( + context, + "Use texture rendering", + kOptionTextureRender, + optGetter: bind.mainGetUseTextureRender, + optSetter: (k, v) async => + await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + ), ), - ), - if (!bind.isCustomClient()) + if (!isWeb && !bind.isCustomClient()) _OptionCheckBox( context, 'Check for software update on startup', @@ -443,7 +511,7 @@ class _GeneralState extends State<_General> { ) ], ]; - if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { + if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { children.add(_OptionCheckBox( context, 'Allow linux headless', kOptionAllowLinuxHeadless)); } @@ -515,16 +583,16 @@ class _GeneralState extends State<_General> { } builder(devices, currentDevice, setDevice) { - return _Card(title: 'Audio Input Device', children: [ - ...devices.map((device) => _Radio(context, - value: device, - groupValue: currentDevice, - autoNewLine: false, - label: device, onChanged: (value) { - setDevice(value); - setState(() {}); - })) - ]); + final child = ComboBox( + keys: devices, + values: devices, + initialKey: currentDevice, + onChanged: (key) async { + setDevice(key); + setState(() {}); + }, + ).marginOnly(left: _kContentHMargin); + return _Card(title: 'Audio Input Device', children: [child]); } return AudioInput(builder: builder, isCm: false, isVoiceCall: false); @@ -539,7 +607,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, @@ -553,12 +620,18 @@ class _GeneralState extends State<_General> { bool root_dir_exists = map['root_dir_exists']!; bool user_dir_exists = map['user_dir_exists']!; return _Card(title: 'Recording', children: [ - _OptionCheckBox(context, 'Automatically record incoming sessions', - kOptionAllowAutoRecordIncoming), - if (showRootDir) + if (!bind.isOutgoingOnly()) + _OptionCheckBox(context, 'Automatically record incoming sessions', + kOptionAllowAutoRecordIncoming), + if (!bind.isIncomingOnly()) + _OptionCheckBox(context, 'Automatically record outgoing sessions', + kOptionAllowAutoRecordOutgoing, + isServer: false), + if (showRootDir && !bind.isOutgoingOnly()) Row( children: [ - Text('${translate("Incoming")}:'), + Text( + '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'), Expanded( child: GestureDetector( onTap: root_dir_exists @@ -575,45 +648,49 @@ class _GeneralState extends State<_General> { ), ], ).marginOnly(left: _kContentHMargin), - Row( - children: [ - Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'), - Expanded( - child: GestureDetector( - onTap: user_dir_exists - ? () => launchUrl(Uri.file(user_dir)) - : null, - child: Text( - user_dir, - softWrap: true, - style: user_dir_exists - ? const TextStyle(decoration: TextDecoration.underline) + if (!(showRootDir && bind.isIncomingOnly())) + Row( + children: [ + Text( + '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'), + Expanded( + child: GestureDetector( + onTap: user_dir_exists + ? () => launchUrl(Uri.file(user_dir)) : null, - )).marginOnly(left: 10), - ), - ElevatedButton( - onPressed: isOptionFixed(kOptionVideoSaveDirectory) - ? null - : () async { - String? initialDirectory; - if (await Directory.fromUri(Uri.directory(user_dir)) - .exists()) { - initialDirectory = user_dir; - } - String? selectedDirectory = - await FilePicker.platform.getDirectoryPath( - initialDirectory: initialDirectory); - if (selectedDirectory != null) { - await bind.mainSetOption( - key: kOptionVideoSaveDirectory, - value: selectedDirectory); - setState(() {}); - } - }, - child: Text(translate('Change'))) - .marginOnly(left: 5), - ], - ).marginOnly(left: _kContentHMargin), + child: Text( + user_dir, + softWrap: true, + style: user_dir_exists + ? const TextStyle( + decoration: TextDecoration.underline) + : null, + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: isOptionFixed(kOptionVideoSaveDirectory) + ? null + : () async { + String? initialDirectory; + if (await Directory.fromUri( + Uri.directory(user_dir)) + .exists()) { + initialDirectory = user_dir; + } + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath( + initialDirectory: initialDirectory); + if (selectedDirectory != null) { + await bind.mainSetLocalOption( + key: kOptionVideoSaveDirectory, + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), ]); }); } @@ -641,8 +718,9 @@ class _GeneralState extends State<_General> { initialKey: currentKey, onChanged: (key) async { await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); - reloadAllWindows(); - bind.mainChangeLanguage(lang: key); + if (isWeb) reloadCurrentWindow(); + if (!isWeb) reloadAllWindows(); + if (!isWeb) bind.mainChangeLanguage(lang: key); }, enabled: !isOptFixed, ).marginOnly(left: _kContentHMargin); @@ -672,29 +750,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - child: Column( - children: [ - _lock(locked, 'Unlock Security Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - permissions(context), - password(context), - _Card(title: '2FA', children: [tfa()]), - _Card(title: 'ID', children: [changeId()]), - more(context), - ]), - ), - ], - )).marginOnly(bottom: _kListViewBottomMargin)); + return SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + preventMouseKeyBuilder( + block: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: '2FA', children: [tfa()]), + _Card(title: 'ID', children: [changeId()]), + more(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin); } Widget tfa() { @@ -783,8 +858,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { onChangedBot(!hasBot.value); }, ).marginOnly(left: _kCheckBoxLeftMargin + 30); + + final trust = Row( + children: [ + Flexible( + child: Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate("enable-trusted-devices-tip"), + child: _OptionCheckBox(context, "Enable trusted devices", + kOptionEnableTrustedDevices, + enabled: !locked, update: (v) { + setState(() {}); + }), + ), + ), + if (mainGetBoolOptionSync(kOptionEnableTrustedDevices)) + ElevatedButton( + onPressed: locked + ? null + : () { + manageTrustedDeviceDialog(); + }, + child: Text(translate('Manage trusted devices'))) + ], + ).marginOnly(left: 30); + return Column( - children: [tfa, bot], + children: [tfa, bot, trust], ); } @@ -971,7 +1071,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate('Accept sessions via both'), ]; var modeInitialKey = model.approveMode; - if (!modeKeys.contains(modeInitialKey)) modeInitialKey = ''; + if (!modeKeys.contains(modeInitialKey)) { + modeInitialKey = defaultOptionApproveMode; + } final usePassword = model.approveMode != 'click'; final isApproveModeFixed = isOptionFixed(kOptionApproveMode); @@ -1018,6 +1120,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'allow-only-conn-window-open-tip', 'allow-only-conn-window-open', reverse: false, enabled: enabled), + if (bind.mainIsInstalled()) unlockPin() ]); } @@ -1085,7 +1188,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 && @@ -1242,7 +1345,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: @@ -1265,6 +1368,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { }(), ]; } + + Widget unlockPin() { + bool enabled = !locked; + RxString unlockPin = bind.mainGetUnlockPin().obs; + update() async { + unlockPin.value = bind.mainGetUnlockPin(); + } + + onChanged(bool? checked) async { + changeUnlockPinDialog(unlockPin.value, update); + } + + final isOptFixed = isOptionFixed(kOptionWhitelist); + return GestureDetector( + child: Obx(() => Row( + children: [ + Checkbox( + value: unlockPin.isNotEmpty, + onChanged: enabled && !isOptFixed ? onChanged : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Unlock with PIN'), + style: TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + )), + onTap: enabled + ? () { + onChanged(!unlockPin.isNotEmpty); + } + : null, + ).marginOnly(left: _kCheckBoxLeftMargin); + } } class _Network extends StatefulWidget { @@ -1277,112 +1414,84 @@ class _Network extends StatefulWidget { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = bind.mainIsInstalled(); + bool locked = !isWeb && bind.mainIsInstalled(); + + final scrollController = ScrollController(); @override Widget build(BuildContext context) { super.build(context); - bool enabled = !locked; - final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + preventMouseKeyBuilder( + block: locked, + child: Column(children: [ + network(context), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget network(BuildContext context) { final hideServer = bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; final hideProxy = - bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - _lock(locked, 'Unlock Network Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - if (!hideServer) server(enabled), - if (!hideProxy) - _Card(title: 'Proxy', children: [ - _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, - enabled: enabled), - ]), - ]), - ), - ]).marginOnly(bottom: _kListViewBottomMargin)); - } + isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - server(bool enabled) { - // Simple temp wrapper for PR check - tmpWrapper() { - // Setting page is not modal, oldOptions should only be used when getting options, never when setting. - Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); - old(String key) { - return (oldOptions[key] ?? '').trim(); - } - - RxString idErrMsg = ''.obs; - RxString relayErrMsg = ''.obs; - RxString apiErrMsg = ''.obs; - var idController = - TextEditingController(text: old('custom-rendezvous-server')); - var relayController = TextEditingController(text: old('relay-server')); - var apiController = TextEditingController(text: old('api-server')); - var keyController = TextEditingController(text: old('key')); - final controllers = [ - idController, - relayController, - apiController, - keyController, - ]; - final errMsgs = [ - idErrMsg, - relayErrMsg, - apiErrMsg, - ]; - - submit() async { - bool result = await setServerConfig( - null, - errMsgs, - ServerConfig( - idServer: idController.text, - relayServer: relayController.text, - apiServer: apiController.text, - key: keyController.text)); - if (result) { - setState(() {}); - showToast(translate('Successful')); - } else { - showToast(translate('Failed')); - } - } - - bool secure = !enabled; - return _Card( - title: 'ID/Relay Server', - title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs), - children: [ - Column( - children: [ - Obx(() => _LabeledTextField(context, 'ID Server', idController, - idErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'API Server', - apiController, apiErrMsg.value, enabled, secure)), - _LabeledTextField( - context, 'Key', keyController, '', enabled, secure), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 10), - ], - ) - ]); + if (hideServer && hideProxy) { + return Offstage(); } - return tmpWrapper(); + return _Card( + title: 'Network', + children: [ + Container( + child: Column( + 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, + onTap: () => showServerSettings(gFFI.dialogManager), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ), + if (!hideServer && !hideProxy) + 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, + onTap: changeSocks5Proxy, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ), + ], + ), + ), + ], + ); } } @@ -1397,19 +1506,14 @@ class _DisplayState extends State<_Display> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - viewStyle(context), - scrollStyle(context), - imageQuality(context), - codec(context), - privacyModeImpl(context), - other(context), - ]).marginOnly(bottom: _kListViewBottomMargin)); + return ListView(controller: scrollController, children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + if (!isWeb) privacyModeImpl(context), + other(context), + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget viewStyle(BuildContext context) { @@ -1632,15 +1736,12 @@ class _AccountState extends State<_Account> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: [ - _Card(title: 'Account', children: [accountAction(), useInfo()]), - ], - ).marginOnly(bottom: _kListViewBottomMargin)); + return ListView( + controller: scrollController, + children: [ + _Card(title: 'Account', children: [accountAction(), useInfo()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin); } Widget accountAction() { @@ -1737,18 +1838,14 @@ class _PluginState extends State<_Plugin> { Widget build(BuildContext context) { bind.pluginListReload(); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ChangeNotifierProvider.value( - value: pluginManager, - child: Consumer(builder: (context, model, child) { - return ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: model.plugins.map((entry) => pluginCard(entry)).toList(), - ).marginOnly(bottom: _kListViewBottomMargin); - }), - ), + return ChangeNotifierProvider.value( + value: pluginManager, + child: Consumer(builder: (context, model, child) { + return ListView( + controller: scrollController, + children: model.plugins.map((entry) => pluginCard(entry)).toList(), + ).marginOnly(bottom: _kListViewBottomMargin); + }), ); } @@ -1800,74 +1897,72 @@ class _AboutState extends State<_About> { final fingerprint = data['fingerprint'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - child: _Card(title: translate('About RustDesk'), children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - SelectionArea( - child: Text('${translate('Version')}: $version') - .marginSymmetric(vertical: 4.0)), - SelectionArea( - child: Text('${translate('Build Date')}: $buildDate') - .marginSymmetric(vertical: 4.0)), - SelectionArea( - child: Text('${translate('Fingerprint')}: $fingerprint') - .marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString('https://rustdesk.com/privacy.html'); - }, - child: Text( - translate('Privacy Statement'), - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString('https://rustdesk.com'); - }, - child: Text( - translate('Website'), - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - Container( - decoration: const BoxDecoration(color: Color(0xFF2c8cff)), - padding: - const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: SelectionArea( - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', - style: const TextStyle(color: Colors.white), - ), - Text( - translate('Slogan_tip'), - style: TextStyle( - fontWeight: FontWeight.w800, - color: Colors.white), - ) - ], + return SingleChildScrollView( + controller: scrollController, + child: _Card(title: translate('About RustDesk'), children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), + if (!isWeb) + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com/privacy.html'); + }, + child: Text( + translate('Privacy Statement'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com'); + }, + child: Text( + translate('Website'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: SelectionArea( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', + style: const TextStyle(color: Colors.white), ), - ), - ], - )), - ).marginSymmetric(vertical: 4.0) - ], - ).marginOnly(left: _kContentHMargin) - ]), - )); + Text( + translate('Slogan_tip'), + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + )), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ); }); } } @@ -2160,9 +2255,14 @@ Widget _lock( Text(translate(label)).marginOnly(left: 5), ]).marginSymmetric(vertical: 2)), onPressed: () async { - bool checked = await callMainCheckSuperUserPermission(); - if (checked) { - onUnlock(); + final unlockPin = bind.mainGetUnlockPin(); + if (unlockPin.isEmpty) { + bool checked = await callMainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + } else { + checkUnlockPinDialog(unlockPin, onUnlock); } }, ).marginSymmetric(horizontal: 2, vertical: 4), @@ -2180,26 +2280,39 @@ _LabeledTextField( String errorText, bool enabled, bool secure) { - return Row( + return Table( + columnWidths: const { + 0: FixedColumnWidth(150), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - '${translate(label)}:', - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 16, color: disabledTextColor(context, enabled)), - ).marginOnly(right: 10)), - Expanded( - child: TextField( + TableRow( + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, + color: disabledTextColor(context, enabled), + ), + ), + ), + TextField( controller: controller, enabled: enabled, obscureText: secure, + autocorrect: false, decoration: InputDecoration( - errorText: errorText.isNotEmpty ? errorText : null), + errorText: errorText.isNotEmpty ? errorText : null, + ), style: TextStyle( color: disabledTextColor(context, enabled), - )), + ), + ).workaroundFreezeLinuxMint(), + ], ), ], ).marginOnly(bottom: 8); @@ -2377,7 +2490,7 @@ void changeSocks5Proxy() async { controller: proxyController, autofocus: true, enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2397,7 +2510,7 @@ void changeSocks5Proxy() async { labelText: isMobile ? translate('Username') : null, ), enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2422,7 +2535,8 @@ void changeSocks5Proxy() async { : Icons.visibility))), controller: pwdController, enabled: !isOptFixed, - )), + maxLength: bind.mainMaxEncryptLen(), + ).workaroundFreezeLinuxMint()), ), ], ), diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 2e577e625ae0..6440e55a1117 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -37,13 +37,9 @@ class DesktopTabPage extends StatefulWidget { } } -class _DesktopTabPageState extends State - with WidgetsBindingObserver { +class _DesktopTabPageState extends State { final tabController = DesktopTabController(tabType: DesktopTabType.main); - final RxBool _block = false.obs; - // bool mouseIn = false; - _DesktopTabPageState() { RemoteCountState.init(); Get.put(tabController); @@ -69,19 +65,10 @@ class _DesktopTabPageState extends State } } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.resumed) { - shouldBeBlocked(_block, canBeBlocked); - } else if (state == AppLifecycleState.inactive) {} - } - @override void initState() { super.initState(); // HardwareKeyboard.instance.addHandler(_handleKeyEvent); - WidgetsBinding.instance.addObserver(this); } /* @@ -97,7 +84,6 @@ class _DesktopTabPageState extends State @override void dispose() { // HardwareKeyboard.instance.removeHandler(_handleKeyEvent); - WidgetsBinding.instance.removeObserver(this); Get.delete(); super.dispose(); @@ -119,13 +105,13 @@ class _DesktopTabPageState extends State isClose: false, ), ), - blockTab: _block, ))); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( () => DragToResizeArea( resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, child: tabWidget, ), ); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3b4428f99e4f..3f555dcaa66e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:extended_text/extended_text.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; @@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget { required this.id, required this.password, required this.isSharedPassword, - required this.tabController, + this.tabController, + this.connToken, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; final bool? forceRelay; - final DesktopTabController tabController; + final String? connToken; + final DesktopTabController? tabController; @override State createState() => _FileManagerPageState(); } class _FileManagerPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final _mouseFocusScope = Rx(MouseFocusScope.none); final _dropMaskVisible = false.obs; // TODO impl drop mask @@ -87,6 +92,7 @@ class _FileManagerPageState extends State isFileTransfer: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + connToken: widget.connToken, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager @@ -96,12 +102,16 @@ class _FileManagerPageState extends State if (!isLinux) { WakelockPlus.enable(); } + if (isWeb) { + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } debugPrint("File manager page init success with id ${widget.id}"); _ffi.dialogManager.setOverlayState(_overlayKeyState); // 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); + widget.tabController?.onSelected?.call(widget.id); }); + WidgetsBinding.instance.addObserver(this); } @override @@ -114,12 +124,21 @@ class _FileManagerPageState extends State } Get.delete(tag: 'ft_${widget.id}'); }); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override bool get wantKeepAlive => true; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + jobController.jobTable.refresh(); + } + } + @override Widget build(BuildContext context) { super.build(context); @@ -129,10 +148,11 @@ class _FileManagerPageState extends State backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ - Flexible( - flex: 3, - child: dropArea(FileManagerView( - model.localController, _ffi, _mouseFocusScope))), + if (!isWeb) + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.localController, _ffi, _mouseFocusScope))), Flexible( flex: 3, child: dropArea(FileManagerView( @@ -173,10 +193,31 @@ class _FileManagerPageState extends State /// transfer status list /// watch transfer status Widget statusList() { + Widget getIcon(JobProgress job) { + final color = Theme.of(context).tabBarTheme.labelColor; + switch (job.type) { + case JobType.deleteDir: + case JobType.deleteFile: + return Icon(Icons.delete_outline, color: color); + default: + return Transform.rotate( + angle: isWeb + ? job.isRemoteToLocal + ? pi / 2 + : pi / 2 * 3 + : job.isRemoteToLocal + ? pi + : 0, + child: Icon(Icons.arrow_forward_ios, color: color), + ); + } + } + statusListView(List jobs) => ListView.builder( controller: ScrollController(), itemBuilder: (BuildContext context, int index) { final item = jobs[index]; + final status = item.getStatus(); return Padding( padding: const EdgeInsets.only(bottom: 5), child: generateCard( @@ -186,15 +227,8 @@ class _FileManagerPageState extends State Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Transform.rotate( - angle: item.isRemoteToLocal ? pi : 0, - child: SvgPicture.asset("assets/arrow.svg", - colorFilter: svgColor( - Theme.of(context).tabBarTheme.labelColor)), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, - ), + getIcon(item) + .marginSymmetric(horizontal: 10, vertical: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -203,45 +237,28 @@ class _FileManagerPageState extends State Tooltip( waitDuration: Duration(milliseconds: 500), message: item.jobName, - child: Text( - item.fileName, + child: ExtendedText( + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, - ).paddingSymmetric(vertical: 10), - ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), - Offstage( - offstage: item.state != JobState.inProgress, - child: Text( - '${translate("Speed")} ${readableFileSize(item.speed)}/s', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), + overflowWidget: TextOverflowWidget( + child: Text("..."), + position: TextOverflowPosition.start), ), ), - Offstage( - offstage: item.state == JobState.inProgress, - child: Text( - translate( - item.display(), - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: status, + child: Text(status, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + )).marginOnly(top: 6), ), Offstage( - offstage: item.state != JobState.inProgress, + offstage: item.type != JobType.transfer || + item.state != JobState.inProgress, child: LinearPercentIndicator( - padding: EdgeInsets.only(right: 15), animateFromLastPercent: true, center: Text( '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', @@ -251,7 +268,7 @@ class _FileManagerPageState extends State progressColor: MyTheme.accent, backgroundColor: Theme.of(context).hoverColor, lineHeight: kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ).paddingSymmetric(vertical: 8), ), ], ), @@ -262,6 +279,7 @@ class _FileManagerPageState extends State Offstage( offstage: item.state != JobState.paused, child: MenuButton( + tooltip: translate("Resume"), onPressed: () { jobController.resumeJob(item.id); }, @@ -274,7 +292,7 @@ class _FileManagerPageState extends State ), ), MenuButton( - padding: EdgeInsets.only(right: 15), + tooltip: translate("Delete"), child: SvgPicture.asset( "assets/close.svg", colorFilter: svgColor(Colors.white), @@ -287,11 +305,11 @@ class _FileManagerPageState extends State hoverColor: MyTheme.accent80, ), ], - ), + ).marginAll(12), ], ), ], - ).paddingSymmetric(vertical: 10), + ), ), ); }, @@ -475,6 +493,9 @@ class _FileManagerViewState extends State { } Widget headTools() { + var uploadButtonTapPosition = RelativeRect.fill; + RxBool isUploadFolder = + (bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs; return Container( child: Column( children: [ @@ -521,6 +542,7 @@ class _FileManagerViewState extends State { Row( children: [ MenuButton( + tooltip: translate('Back'), padding: EdgeInsets.only( right: 3, ), @@ -540,6 +562,7 @@ class _FileManagerViewState extends State { }, ), MenuButton( + tooltip: translate('Parent directory'), child: RotatedBox( quarterTurns: 3, child: SvgPicture.asset( @@ -604,6 +627,7 @@ class _FileManagerViewState extends State { switch (_locationStatus.value) { case LocationStatus.bread: return MenuButton( + tooltip: translate('Search'), onPressed: () { _locationStatus.value = LocationStatus.fileSearchBar; Future.delayed( @@ -630,6 +654,7 @@ class _FileManagerViewState extends State { ); case LocationStatus.fileSearchBar: return MenuButton( + tooltip: translate('Clear'), onPressed: () { onSearchText("", isLocal); _locationStatus.value = LocationStatus.bread; @@ -645,6 +670,7 @@ class _FileManagerViewState extends State { } }), MenuButton( + tooltip: translate('Refresh File'), padding: EdgeInsets.only( left: 3, ), @@ -670,6 +696,7 @@ class _FileManagerViewState extends State { isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ MenuButton( + tooltip: translate('Home'), padding: EdgeInsets.only( right: 3, ), @@ -685,11 +712,27 @@ class _FileManagerViewState extends State { hoverColor: Theme.of(context).hoverColor, ), MenuButton( + tooltip: translate('Create Folder'), onPressed: () { final name = TextEditingController(); + String? errorText; _ffi.dialogManager.show((setState, close, context) { + name.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); submit() { if (name.value.text.isNotEmpty) { + if (!PathUtil.validName(name.value.text, + controller.options.value.isWindows)) { + setState(() { + errorText = translate("Invalid folder name"); + }); + return; + } controller.createDir(PathUtil.join( controller.directory.value.path, name.value.text, @@ -721,10 +764,11 @@ class _FileManagerViewState extends State { labelText: translate( "Please enter the folder name", ), + errorText: errorText, ), controller: name, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ], ), actions: [ @@ -754,6 +798,7 @@ class _FileManagerViewState extends State { hoverColor: Theme.of(context).hoverColor, ), Obx(() => MenuButton( + tooltip: translate('Delete'), onPressed: SelectedItems.valid(selectedItems.items) ? () async { await (controller @@ -773,6 +818,66 @@ class _FileManagerViewState extends State { ], ), ), + if (isWeb) + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: () => + {webselectFiles(is_folder: isUploadFolder.value)}, + label: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + uploadButtonTapPosition = + RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final value = await showMenu( + context: context, + position: uploadButtonTapPosition, + items: [ + PopupMenuItem( + value: false, + child: Text(translate('Upload files')), + ), + PopupMenuItem( + value: true, + child: Text(translate('Upload folder')), + ), + ]); + if (value != null) { + isUploadFolder.value = value; + bind.mainSetLocalOption( + key: 'upload-folder-button', + value: value ? 'Y' : ''); + webselectFiles(is_folder: value); + } + }, + child: Icon(Icons.arrow_drop_down), + ), + icon: Text( + translate(isUploadFolder.isTrue + ? 'Upload folder' + : 'Upload files'), + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white, + ), + ).marginOnly(left: 8), + )).marginOnly(left: 16), Obx(() => ElevatedButton.icon( style: ButtonStyle( padding: MaterialStateProperty.all( @@ -806,19 +911,22 @@ class _FileManagerViewState extends State { : Colors.white, ), ) - : RotatedBox( - quarterTurns: 2, - child: SvgPicture.asset( - "assets/arrow.svg", - colorFilter: svgColor(selectedItems.items.isEmpty - ? Theme.of(context).brightness == - Brightness.light - ? MyTheme.grayBg - : MyTheme.darkGray - : Colors.white), - alignment: Alignment.bottomRight, - ), - ), + : isWeb + ? Offstage() + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor( + selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white), + alignment: Alignment.bottomRight, + ), + ), label: isLocal ? SvgPicture.asset( "assets/arrow.svg", @@ -830,7 +938,7 @@ class _FileManagerViewState extends State { : Colors.white), ) : Text( - translate('Receive'), + translate(isWeb ? 'Download' : 'Receive'), style: TextStyle( color: selectedItems.items.isEmpty ? Theme.of(context).brightness == @@ -885,6 +993,7 @@ class _FileManagerViewState extends State { menuPos = RelativeRect.fromLTRB(x, y, x, y); }, child: MenuButton( + tooltip: translate('More'), onPressed: () => mod_menu.showMenu( context: context, position: menuPos, @@ -916,6 +1025,7 @@ class _FileManagerViewState extends State { BuildContext context, ScrollController scrollController) { final fd = controller.directory.value; final entries = fd.entries; + Rx rightClickEntry = Rx(null); return ListSearchActionListener( node: _keyboardNode, @@ -974,16 +1084,70 @@ class _FileManagerViewState extends State { final lastModifiedStr = entry.isDrive ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; + var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); + onTap() { + final items = selectedItems; + // handle double click + if (_checkDoubleClick(entry)) { + controller.openDirectory(entry.path); + items.clear(); + return; + } + _onSelectedChanged(items, filteredEntries, entry, isLocal); + } + + onSecondaryTap() { + final items = [ + if (!entry.isDrive && + versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0) + mod_menu.PopupMenuItem( + child: Text(translate("Rename")), + height: CustomPopupMenuTheme.height, + onTap: () { + controller.renameAction(entry, isLocal); + }, + ) + ]; + if (items.isNotEmpty) { + rightClickEntry.value = entry; + final future = mod_menu.showMenu( + context: context, + position: secondaryPosition, + items: items, + ); + future.then((value) { + rightClickEntry.value = null; + }); + future.onError((error, stackTrace) { + rightClickEntry.value = null; + }); + } + } + + onSecondaryTapDown(details) { + secondaryPosition = RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy); + } + return Padding( padding: EdgeInsets.symmetric(vertical: 1), child: Obx(() => Container( decoration: BoxDecoration( color: selectedItems.items.contains(entry) - ? Theme.of(context).hoverColor + ? MyTheme.button : Theme.of(context).cardColor, borderRadius: BorderRadius.all( Radius.circular(5.0), ), + border: rightClickEntry.value == entry + ? Border.all( + color: MyTheme.button, + width: 1.0, + ) + : null, ), key: ValueKey(entry.name), height: kDesktopFileTransferRowHeight, @@ -1022,22 +1186,19 @@ class _FileManagerViewState extends State { ), Expanded( child: Text(entry.name.nonBreaking, + style: TextStyle( + color: selectedItems.items + .contains(entry) + ? Colors.white + : null), overflow: TextOverflow.ellipsis)) ]), )), ), - onTap: () { - final items = selectedItems; - // handle double click - if (_checkDoubleClick(entry)) { - controller.openDirectory(entry.path); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), SizedBox( width: 2.0, @@ -1054,11 +1215,17 @@ class _FileManagerViewState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, - color: MyTheme.darkGray, + color: selectedItems.items + .contains(entry) + ? Colors.white70 + : MyTheme.darkGray, ), )), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), // Divider from header. SizedBox( @@ -1074,9 +1241,16 @@ class _FileManagerViewState extends State { sizeStr, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), + fontSize: 10, + color: + selectedItems.items.contains(entry) + ? Colors.white70 + : MyTheme.darkGray), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), ), ], @@ -1483,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 a68e4feecdc3..cc77cdd95819 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State { super.initState(); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); // for simplify, just replace connectionId if (call.method == kWindowEventNewFileTransfer) { @@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); @@ -111,6 +113,7 @@ class _FileManagerTabPageState extends State { : SubWindowDragToResizeArea( child: tabWidget, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, ); } diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index a860fe89ea1c..756367c21f1f 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; @@ -41,6 +43,7 @@ class _InstallPageState extends State { Widget build(BuildContext context) { return DragToResizeArea( resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, child: Container( child: Scaffold( backgroundColor: Theme.of(context).colorScheme.background, @@ -73,6 +76,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> _InstallPageBodyState() { controller = TextEditingController(text: bind.installInstallPath()); + final installOptions = jsonDecode(bind.installInstallOptions()); + startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; + desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; } @override @@ -141,7 +147,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> decoration: InputDecoration( contentPadding: EdgeInsets.all(0.75 * em), ), - ).marginOnly(right: 10), + ).workaroundFreezeLinuxMint().marginOnly(right: 10), ), Obx( () => OutlinedButton.icon( @@ -249,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> if (desktopicon.value) args += ' desktopicon'; bind.installInstallMe(options: args, path: controller.text); } + do_install(); } diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 5541cb8b33b9..6671d041bbf5 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget { required this.isRDP, required this.isSharedPassword, this.forceRelay, + this.connToken, }) : super(key: key); final String id; final String? password; @@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget { final bool isRDP; final bool? forceRelay; final bool? isSharedPassword; + final String? connToken; @override State createState() => _PortForwardPageState(); @@ -62,6 +64,7 @@ class _PortForwardPageState extends State password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, + connToken: widget.connToken, isRdp: widget.isRDP); Get.put(_ffi, tag: 'pf_${widget.id}'); debugPrint("Port forward page init success with id ${widget.id}"); @@ -235,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 5534db85549b..f399f7cab687 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State { tabController: tabController, isRDP: isRDP, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State { isRDP: isRDP, tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); @@ -127,6 +129,7 @@ class _PortForwardTabPageState extends State { () => SubWindowDragToResizeArea( child: tabWidget, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, ), ); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 341025c5f3c9..912b06b0282b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; @@ -115,6 +114,8 @@ class _RemotePageState extends State _ffi.imageModel.addCallbackOnFirstImage((String peerId) { showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); }); _ffi.start( widget.id, @@ -245,13 +246,14 @@ class _RemotePageState extends State super.dispose(); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); _ffi.textureModel.onRemotePageDispose(closeSession); - // ensure we leave this session, this is a double check - _ffi.inputModel.enterOrLeave(false); + 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(); - _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); await _ffi.close(closeSession: closeSession); _timer?.cancel(); @@ -739,12 +741,6 @@ class _ImagePaintState extends State { ScrollController horizontal, ScrollController vertical, ) { - final scrollConfig = CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier); var widget = child; if (layoutSize.width < size.width) { widget = ScrollConfiguration( @@ -790,36 +786,26 @@ class _ImagePaintState extends State { ); } if (layoutSize.width < size.width) { - widget = ImprovedScrolling( - scrollController: horizontal, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: horizontal, - thumbVisibility: false, - trackVisibility: false, - notificationPredicate: layoutSize.height < size.height - ? (notification) => notification.depth == 1 - : defaultScrollNotificationPredicate, - child: widget, - ), + 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 = ImprovedScrolling( - scrollController: vertical, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: vertical, - thumbVisibility: false, - trackVisibility: false, - child: widget, - ), + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, ); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 94535bc6dcfc..efd437e1ff74 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -228,6 +228,7 @@ class _ConnectionTabPageState extends State { // Specially configured for a better resize area and remote control. childPadding: kDragToResizeAreaPadding, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, )); } @@ -394,7 +395,7 @@ class _ConnectionTabPageState extends State { RemoteCountState.find().value = tabController.length; Future _remoteMethodHandler(call, fromWindowId) async { - print( + debugPrint( "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); dynamic returnValue; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index a052437479b7..95d9f2c7c7d5 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -83,7 +83,7 @@ class _DesktopServerPageState extends State child: Consumer( builder: (context, serverModel, child) { final body = Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: ConnectionManager(), ); return isLinux @@ -110,7 +110,8 @@ class ConnectionManager extends StatefulWidget { class ConnectionManagerState extends State with WidgetsBindingObserver { - final RxBool _block = false.obs; + final RxBool _controlPageBlock = false.obs; + final RxBool _sidePageBlock = false.obs; ConnectionManagerState() { gFFI.serverModel.tabController.onSelected = (client_id_str) { @@ -139,7 +140,8 @@ class ConnectionManagerState extends State super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { if (!allowRemoteCMModification()) { - shouldBeBlocked(_block, null); + shouldBeBlocked(_controlPageBlock, null); + shouldBeBlocked(_sidePageBlock, null); } } } @@ -192,9 +194,6 @@ class ConnectionManagerState extends State selectedBorderColor: MyTheme.accent, maxLabelWidth: 100, tail: null, //buildScrollJumper(), - blockTab: allowRemoteCMModification() ? null : _block, - selectedTabBackgroundColor: - Theme.of(context).hintColor.withOpacity(0), tabBuilder: (key, icon, label, themeConf) { final client = serverModel.clients .firstWhereOrNull((client) => client.id.toString() == key); @@ -229,7 +228,7 @@ class ConnectionManagerState extends State borderWidth; final realChatPageWidth = constrains.maxWidth - realClosedWidth; - return Row(children: [ + final row = Row(children: [ if (constrains.maxWidth > kConnectionManagerWindowSizeClosedChat.width) Consumer( @@ -239,14 +238,25 @@ class ConnectionManagerState extends State ? buildSidePage() : buildRemoteBlock( child: buildSidePage(), - block: _block, + block: _sidePageBlock, mask: true), )), SizedBox( width: realClosedWidth, - child: - SizedBox(width: realClosedWidth, child: pageView)), + child: SizedBox( + width: realClosedWidth, + child: allowRemoteCMModification() + ? pageView + : buildRemoteBlock( + child: _buildKeyEventBlock(pageView), + block: _controlPageBlock, + mask: false, + ))), ]); + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: row, + ); }, ), ), @@ -266,6 +276,10 @@ class ConnectionManagerState extends State } } + Widget _buildKeyEventBlock(Widget child) { + return ExcludeFocus(child: child, excluding: true); + } + Widget buildTitleBar() { return SizedBox( height: kDesktopRemoteTabBarHeight, @@ -1155,6 +1169,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> { Text(translate('Create Folder')) ], ); + case CmFileAction.rename: + return Column( + children: [ + Icon( + Icons.drive_file_move_outlined, + color: Theme.of(context).tabBarTheme.labelColor, + ), + Text(translate('Rename')) + ], + ); } } diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 7828dd4a0ec6..0984eea5801c 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) { localPlatform = kPeerPlatformWindows; } else if (isLinux) { localPlatform = kPeerPlatformLinux; + } else if (isWebOnWindows || isWebOnLinux) { + localPlatform = kPeerPlatformWebDesktop; } - // to-do: web desktop support ? return localPlatform; } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 17b160fed0ac..8fc90de114b9 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -34,6 +34,7 @@ class _MenuButtonState extends State { return Padding( padding: widget.padding, child: Tooltip( + waitDuration: Duration(milliseconds: 300), message: widget.tooltip, child: Material( type: MaterialType.transparency, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 98fa676144b4..d826ea8c6b60 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -305,7 +305,7 @@ class RemoteMenuEntry { }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', + translate("Insert Ctrl + Alt + Del"), style: style, ), proc: () { @@ -436,6 +436,7 @@ class _RemoteToolbarState extends State { shadowColor: MyTheme.color(context).shadow, borderRadius: borderRadius, child: _DraggableShowHide( + id: widget.id, sessionId: widget.ffi.sessionId, dragging: _dragging, fractionX: _fractionX, @@ -452,8 +453,8 @@ class _RemoteToolbarState extends State { Widget _buildToolbar(BuildContext context) { final List toolbarItems = []; + toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { - toolbarItems.add(_PinMenu(state: widget.state)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); } @@ -478,8 +479,8 @@ class _RemoteToolbarState extends State { setFullscreen: _setFullscreen, )); toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { - toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); } if (!isWeb) toolbarItems.add(_RecordMenu()); @@ -1494,7 +1495,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { ); } - TextField _resolutionInput(TextEditingController controller) { + Widget _resolutionInput(TextEditingController controller) { return TextField( decoration: InputDecoration( border: InputBorder.none, @@ -1508,7 +1509,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), ], controller: controller, - ); + ).workaroundFreezeLinuxMint(); } List _supportedResolutionMenuButtons() => resolutions @@ -1612,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget { // If use flutter to grab keys, we can only use one mode. // Map mode and Legacy mode, at least one of them is supported. String? modeOnly; - if (isInputSourceFlutter) { + // Keep both map and legacy mode on web at the moment. + // TODO: Remove legacy mode after web supports translate mode on web. + if (isInputSourceFlutter && isDesktop) { if (bind.sessionIsKeyboardModeSupported( sessionId: ffi.sessionId, mode: kKeyMapMode)) { modeOnly = kKeyMapMode; @@ -1716,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget { if (value == null) return; await bind.sessionToggleOption( sessionId: ffi.sessionId, value: kOptionToggleViewOnly); - ffiModel.setViewOnly(id, value); + final viewOnly = await bind.sessionGetToggleOption( + sessionId: ffi.sessionId, arg: kOptionToggleViewOnly); + ffiModel.setViewOnly(id, viewOnly ?? value); } : null, ffi: ffi, @@ -1776,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> { @override Widget build(BuildContext context) { - return _IconSubmenuButton( - tooltip: 'Chat', - key: chatButtonKey, - svg: 'assets/chat.svg', - ffi: widget.ffi, - color: _ToolbarTheme.blueColor, - hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildrenGetter: () => [textChat(), voiceCall()]); + if (isWeb) { + return buildTextChatButton(); + } else { + return _IconSubmenuButton( + tooltip: 'Chat', + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + menuChildrenGetter: () => [textChat(), voiceCall()]); + } + } + + buildTextChatButton() { + return _IconMenuButton( + assetName: 'assets/message_24dp_5F6368.svg', + tooltip: 'Text chat', + key: chatButtonKey, + onPressed: _textChatOnPressed, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + ); } textChat() { return MenuButton( child: Text(translate('Text chat')), ffi: widget.ffi, - onPressed: () { - RenderBox? renderBox = - chatButtonKey.currentContext?.findRenderObject() as RenderBox?; - - Offset? initPos; - if (renderBox != null) { - final pos = renderBox.localToGlobal(Offset.zero); - initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); - } + onPressed: _textChatOnPressed); + } - widget.ffi.chatModel.changeCurrentKey( - MessageKey(widget.ffi.id, ChatModel.clientModeID)); - widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); - }); + _textChatOnPressed() { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); + } + widget.ffi.chatModel + .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID)); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); } voiceCall() { @@ -1904,8 +1924,7 @@ class _RecordMenu extends StatelessWidget { var ffi = Provider.of(context); var recordingModel = Provider.of(context); final visible = - (recordingModel.start || ffi.permissions['recording'] != false) && - ffi.pi.currentDisplay != kAllDisplayValue; + (recordingModel.start || ffi.permissions['recording'] != false); if (!visible) return Offstage(); return _IconMenuButton( assetName: 'assets/rec.svg', @@ -2214,6 +2233,7 @@ class RdoMenuButton extends StatelessWidget { } class _DraggableShowHide extends StatefulWidget { + final String id; final SessionID sessionId; final RxDouble fractionX; final RxBool dragging; @@ -2225,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget { const _DraggableShowHide({ Key? key, + required this.id, required this.sessionId, required this.fractionX, required this.dragging, @@ -2314,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); final isFullscreen = stateGlobal.fullscreen; const double iconSize = 20; + + buttonWrapper(VoidCallback? onPressed, Widget child, + {Color hoverColor = _ToolbarTheme.blueColor}) { + final bgColor = buttonStyle.backgroundColor?.resolve({}); + return TextButton( + onPressed: onPressed, + child: child, + style: buttonStyle.copyWith( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.hovered)) { + return (bgColor ?? hoverColor).withOpacity(0.15); + } + return bgColor; + }), + ), + ); + } + final child = Row( mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), - Obx(() => TextButton( - onPressed: () { + Obx(() => buttonWrapper( + () { widget.setFullscreen(!isFullscreen.value); }, - child: Tooltip( + Tooltip( message: translate( isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), child: Icon( @@ -2333,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - if (!isMacOS) + if (!isMacOS && !isWebDesktop) Obx(() => Offstage( offstage: isFullscreen.isFalse, - child: TextButton( - onPressed: () => widget.setMinimize(), - child: Tooltip( + child: buttonWrapper( + widget.setMinimize, + Tooltip( message: translate('Minimize'), child: Icon( Icons.remove, @@ -2347,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - TextButton( - onPressed: () => setState(() { + buttonWrapper( + () => setState(() { widget.toolbarState.switchShow(widget.sessionId); }), - child: Obx((() => Tooltip( + Obx((() => Tooltip( message: translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( @@ -2360,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ))), ), + if (isWebDesktop) + Obx(() { + if (show.isTrue) { + return Offstage(); + } else { + return buttonWrapper( + () => closeConnection(id: widget.id), + Tooltip( + message: translate('Close'), + child: Icon( + Icons.close, + size: iconSize, + color: _ToolbarTheme.redColor, + ), + ), + hoverColor: _ToolbarTheme.redColor, + ).paddingOnly(left: iconSize / 2); + } + }) ], ); return TextButtonTheme( diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart deleted file mode 100644 index c5bc3394b439..000000000000 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; - -class DesktopScrollWrapper extends StatelessWidget { - final ScrollController scrollController; - final Widget child; - const DesktopScrollWrapper( - {Key? key, required this.scrollController, required this.child}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return ImprovedScrolling( - scrollController: scrollController, - enableCustomMouseWheelScrolling: true, - // enableKeyboardScrolling: true, // strange behavior on mac - customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier), - child: child, - ); - } -} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index fca6efb2e9ba..96ada22c9072 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -246,7 +246,6 @@ class DesktopTab extends StatefulWidget { final Color? selectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor; final Color? selectedBorderColor; - final RxBool? blockTab; final DesktopTabController controller; @@ -272,7 +271,6 @@ class DesktopTab extends StatefulWidget { this.selectedTabBackgroundColor, this.unSelectedTabBackgroundColor, this.selectedBorderColor, - this.blockTab, }) : super(key: key); static RxString tablabelGetter(String peerId) { @@ -311,7 +309,6 @@ class _DesktopTabState extends State Color? get unSelectedTabBackgroundColor => widget.unSelectedTabBackgroundColor; Color? get selectedBorderColor => widget.selectedBorderColor; - RxBool? get blockTab => widget.blockTab; DesktopTabController get controller => widget.controller; RxList get invisibleTabKeys => widget.invisibleTabKeys; Debouncer get _scrollDebounce => widget._scrollDebounce; @@ -505,17 +502,20 @@ class _DesktopTabState extends State Obx(() { if (stateGlobal.showTabBar.isTrue && !(kUseCompatibleUiMode && isHideSingleItem())) { + final showBottomDivider = _showTabBarBottomDivider(tabType); return SizedBox( height: _kTabBarHeight, child: Column( children: [ SizedBox( - height: _kTabBarHeight - 1, + height: + showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight, child: _buildBar(), ), - const Divider( - height: 1, - ), + if (showBottomDivider) + const Divider( + height: 1, + ), ], ), ); @@ -530,25 +530,20 @@ class _DesktopTabState extends State ]); } - Widget _buildBlock({required Widget child}) { - if (blockTab != null) { - return buildRemoteBlock( - child: child, - block: blockTab!, - use: canBeBlocked, - mask: tabType == DesktopTabType.main); - } else { - return child; - } - } - List _tabWidgets = []; Widget _buildPageView() { - final child = _buildBlock( + final child = Container( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), children: () { + if (DesktopTabType.cm == tabType) { + // Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful. + return state.value.tabs.map((tab) { + return tab.page; + }).toList(); + } + /// to-do refactor, separate connection state and UI state for remote session. /// [workaround] PageView children need an immutable list, after it has been passed into PageView final tabLen = state.value.tabs.length; @@ -1161,7 +1156,10 @@ class _TabState extends State<_Tab> with RestorationMixin { child: Row( children: [ SizedBox( - height: _kTabBarHeight, + // _kTabBarHeight also displays normally + height: _showTabBarBottomDivider(widget.tabType) + ? _kTabBarHeight - 1 + : _kTabBarHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1414,6 +1412,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> { } } +bool _showTabBarBottomDivider(DesktopTabType tabType) { + return tabType == DesktopTabType.main || tabType == DesktopTabType.install; +} + class TabbarTheme extends ThemeExtension { final Color? selectedTabIconColor; final Color? unSelectedTabIconColor; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0386d63cf50f..3032a2321f00 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -36,6 +36,7 @@ WindowType? kWindowType; late List kBootArgs; Future main(List args) async { + earlyAssert(); WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); @@ -119,6 +120,7 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { // register uni links await initEnv(kAppTypeMain); + checkUpdate(); // trigger connection status updater await bind.mainCheckConnectStatus(); if (startService) { @@ -155,13 +157,14 @@ void runMainApp(bool startService) async { void runMobileApp() async { await initEnv(kAppTypeMain); + checkUpdate(); if (isAndroid) androidChannelInit(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); draggablePositions.load(); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); gFFI.userModel.refreshCurrentUser(); runApp(App()); - if (!isWeb) await initUniLinks(); + await initUniLinks(); } void runMultiWindow( @@ -260,7 +263,7 @@ showCmWindow({bool isStartup = false}) async { WindowOptions windowOptions = getHiddenTitleBarWindowOptions( size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true); await windowManager.waitUntilReadyToShow(windowOptions, null); - bind.mainHideDocker(); + bind.mainHideDock(); await Future.wait([ windowManager.show(), windowManager.focus(), @@ -288,14 +291,14 @@ hideCmWindow({bool isStartup = false}) async { size: kConnectionManagerWindowSizeClosedChat); windowManager.setOpacity(0); await windowManager.waitUntilReadyToShow(windowOptions, null); - bind.mainHideDocker(); + bind.mainHideDock(); await windowManager.minimize(); await windowManager.hide(); _isCmReadyToShow = true; } else if (_isCmReadyToShow) { if (await windowManager.getOpacity() != 0) { await windowManager.setOpacity(0); - bind.mainHideDocker(); + bind.mainHideDock(); await windowManager.minimize(); await windowManager.hide(); } @@ -372,7 +375,7 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -396,6 +399,34 @@ class _AppState extends State { bind.mainChangeTheme(dark: to.toShortString()); } }; + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation()); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + _updateOrientation(); + } + + void _updateOrientation() { + if (isDesktop) return; + + // Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`, + // my test (Flutter 3.19.6, Android 14) is always the reverse value. + // https://github.com/flutter/flutter/issues/60899 + // stateGlobal.isPortrait.value = + // MediaQuery.of(context).orientation == Orientation.portrait; + + final orientation = View.of(context).physicalSize.aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait; + stateGlobal.isPortrait.value = orientation == Orientation.portrait; } @override @@ -416,7 +447,9 @@ class _AppState extends State { child: GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, - title: 'RustDesk', + title: isWeb + ? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)' + : bind.mainGetAppNameSync(), theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, themeMode: MyTheme.currentThemeMode(), @@ -447,7 +480,8 @@ class _AppState extends State { : (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); - if (isDesktop && desktopType == DesktopType.main) { + if ((isDesktop && desktopType == DesktopType.main) || + isWebDesktop) { child = keyListenerBuilder(context, child); } if (isLinux) { @@ -475,7 +509,7 @@ _registerEventHandler() { platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); } }); platformFFI.registerEventHandler('language', 'language', (_) async { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 02c552d716d7..1d83b5744c34 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -3,25 +3,24 @@ import 'dart:async'; import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import '../../common.dart'; -import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; -import 'scan_page.dart'; -import 'settings_page.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { - ConnectionPage({Key? key}) : super(key: key); + ConnectionPage({Key? key, required this.appBarActions}) : super(key: key); @override final icon = const Icon(Icons.connected_tv); @@ -30,7 +29,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { final title = translate("Connection"); @override - final appBarActions = isWeb ? [const WebMenu()] : []; + final List appBarActions; @override State createState() => _ConnectionPageState(); @@ -42,14 +41,15 @@ class _ConnectionPageState extends State { final _idController = IDTextEditingController(); final RxBool _idEmpty = true.obs; - /// Update url. If it's not null, means an update is available. - var _updateUrl = ''; List peers = []; bool isPeersLoading = false; bool isPeersLoaded = false; StreamSubscription? _uniLinksSubscription; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; + _ConnectionPageState() { if (!isWeb) _uniLinksSubscription = listenUniLinks(); _idController.addListener(() { @@ -71,14 +71,6 @@ class _ConnectionPageState extends State { } }); } - if (isAndroid) { - if (!bind.isCustomClient()) { - Timer(const Duration(seconds: 1), () async { - _updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (_updateUrl.isNotEmpty) setState(() {}); - }); - } - } } @override @@ -88,7 +80,8 @@ class _ConnectionPageState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - if (!bind.isCustomClient()) _buildUpdateUI(), + if (!bind.isCustomClient() && !isIOS) + Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)), _buildRemoteIDTextField(), ])), SliverFillRemaining( @@ -107,16 +100,22 @@ class _ConnectionPageState extends State { } /// UI for software update. - /// If [_updateUrl] is not empty, shows a button to update the software. - Widget _buildUpdateUI() { - return _updateUrl.isEmpty + /// If _updateUrl] is not empty, shows a button to update the software. + Widget _buildUpdateUI(String updateUrl) { + return updateUrl.isEmpty ? const SizedBox(height: 0) : InkWell( onTap: () async { final url = 'https://rustdesk.com/download'; - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + // 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 + // 1. The following check + // 2. `` in AndroidManifest.xml + // + // But it is better to add the check. + await launchUrl(Uri.parse(url)); }, child: Container( alignment: AlignmentDirectional.center, @@ -160,7 +159,7 @@ class _ConnectionPageState extends State { child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -176,7 +175,7 @@ class _ConnectionPageState extends State { rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -188,7 +187,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -200,12 +199,15 @@ class _ConnectionPageState extends State { peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { fieldTextEditingController.text = _idController.text; + Get.put( + fieldTextEditingController); fieldFocusNode.addListener(() async { _idEmpty.value = fieldTextEditingController.text.isEmpty; @@ -252,6 +254,9 @@ class _ConnectionPageState extends State { ), ), inputFormatters: [IDTextInputFormatter()], + onSubmitted: (_) { + onConnect(); + }, ); }, onSelected: (option) { @@ -263,6 +268,7 @@ class _ConnectionPageState extends State { optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; @@ -341,9 +347,15 @@ class _ConnectionPageState extends State { ), ), ); + final child = Column(children: [ + if (isWebDesktop) + getConnectionPageTitle(context, true) + .marginOnly(bottom: 10, top: 15, left: 12), + w + ]); return Align( alignment: Alignment.topCenter, - child: Container(constraints: kMobilePageConstraints, child: w)); + child: Container(constraints: kMobilePageConstraints, child: child)); } @override @@ -353,76 +365,9 @@ class _ConnectionPageState extends State { if (Get.isRegistered()) { Get.delete(); } + if (Get.isRegistered()) { + Get.delete(); + } super.dispose(); } } - -class WebMenu extends StatefulWidget { - const WebMenu({Key? key}) : super(key: key); - - @override - State createState() => _WebMenuState(); -} - -class _WebMenuState extends State { - @override - Widget build(BuildContext context) { - Provider.of(context); - return PopupMenuButton( - tooltip: "", - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return (isIOS - ? [ - const PopupMenuItem( - value: "scan", - child: Icon(Icons.qr_code_scanner, color: Colors.black), - ) - ] - : >[]) + - [ - PopupMenuItem( - value: "server", - child: Text(translate('ID/Relay Server')), - ) - ] + - [ - PopupMenuItem( - value: "login", - child: Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})'), - ) - ] + - [ - PopupMenuItem( - value: "about", - child: Text(translate('About RustDesk')), - ) - ]; - }, - onSelected: (value) { - if (value == 'server') { - showServerSettings(gFFI.dialogManager); - } - if (value == 'about') { - showAbout(gFFI.dialogManager); - } - if (value == 'login') { - if (gFFI.userModel.userName.value.isEmpty) { - loginDialog(); - } else { - logOutConfirmDialog(); - } - } - if (value == 'scan') { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ScanPage(), - ), - ); - } - }); - } -} diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index b74d44484fc2..b837dc276e39 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -204,36 +204,54 @@ class _FileManagerPageState extends State { setState(() {}); } else if (v == "folder") { final name = TextEditingController(); - gFFI.dialogManager - .show((setState, close, context) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], + String? errorText; + gFFI.dialogManager.show((setState, close, context) { + name.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); + return CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: + translate("Please enter the folder name"), + errorText: errorText, ), - actions: [ - dialogButton("Cancel", - onPressed: () => close(false), - isOutline: true), - dialogButton("OK", onPressed: () { - if (name.value.text.isNotEmpty) { - currentFileController.createDir( - PathUtil.join( - currentDir.path, - name.value.text, - currentOptions.isWindows)); - close(); - } - }) - ])); + controller: name, + ).workaroundFreezeLinuxMint(), + ], + ), + actions: [ + dialogButton("Cancel", + onPressed: () => close(false), isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + if (!PathUtil.validName( + name.value.text, + currentFileController + .options.value.isWindows)) { + setState(() { + errorText = + translate("Invalid folder name"); + }); + return; + } + currentFileController.createDir(PathUtil.join( + currentDir.path, + name.value.text, + currentOptions.isWindows)); + close(); + } + }) + ]); + }); } else if (v == "hidden") { currentFileController.toggleShowHidden(); } @@ -497,7 +515,15 @@ class _FileManagerViewState extends State { child: Text(translate("Properties")), value: "properties", enabled: false, - ) + ), + if (!entries[index].isDrive && + versionCmp(gFFI.ffiModel.pi.version, + "1.3.0") >= + 0) + PopupMenuItem( + child: Text(translate("Rename")), + value: "rename", + ) ]; }, onSelected: (v) { @@ -509,6 +535,9 @@ class _FileManagerViewState extends State { _selectedItems.clear(); widget.selectMode.toggle(isLocal); setState(() {}); + } else if (v == "rename") { + controller.renameAction( + entries[index], isLocal); } }), onTap: () { diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 078e2b2f7b3c..efccc5de65ef 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import 'package:flutter_hbb/web/settings_page.dart'; import 'package:get/get.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; import '../../models/platform_model.dart'; +import '../../models/state_model.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -45,7 +47,11 @@ class HomePageState extends State { void initPages() { _pages.clear(); - if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); + if (!bind.isIncomingOnly()) { + _pages.add(ConnectionPage( + appBarActions: [], + )); + } if (isAndroid && !bind.isOutgoingOnly()) { _chatPageTabIndex = _pages.length; _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); @@ -149,18 +155,80 @@ class HomePageState extends State { } class WebHomePage extends StatelessWidget { - final connectionPage = ConnectionPage(); + final connectionPage = + ConnectionPage(appBarActions: [const WebSettingsPage()]); @override Widget build(BuildContext context) { + stateGlobal.isInMainPage = true; + handleUnilink(context); return Scaffold( // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, - title: Text(bind.mainGetAppNameSync()), + title: Text("${bind.mainGetAppNameSync()} (Preview)"), actions: connectionPage.appBarActions, ), body: connectionPage, ); } + + handleUnilink(BuildContext context) { + if (webInitialLink.isEmpty) { + return; + } + final link = webInitialLink; + webInitialLink = ''; + final splitter = ["/#/", "/#", "#/", "#"]; + var fakelink = ''; + for (var s in splitter) { + if (link.contains(s)) { + var list = link.split(s); + if (list.length < 2 || list[1].isEmpty) { + return; + } + list.removeAt(0); + fakelink = "rustdesk://${list.join(s)}"; + break; + } + } + if (fakelink.isEmpty) { + return; + } + final uri = Uri.tryParse(fakelink); + if (uri == null) { + return; + } + final args = urlLinkToCmdArgs(uri); + if (args == null || args.isEmpty) { + return; + } + bool isFileTransfer = 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; + case '--file-transfer': + isFileTransfer = true; + id = args[i + 1]; + i++; + break; + case '--password': + password = args[i + 1]; + i++; + break; + default: + break; + } + } + if (id != null) { + connect(context, id, isFileTransfer: isFileTransfer, password: password); + } + } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74b56cd45fc6..27ce22713803 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,6 +26,19 @@ import '../widgets/dialog.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 RemotePage extends StatefulWidget { RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) : super(key: key); @@ -37,12 +51,15 @@ class RemotePage extends StatefulWidget { State createState() => _RemotePageState(id); } -class _RemotePageState extends State { +class _RemotePageState extends State with WidgetsBindingObserver { Timer? _timer; bool _showBar = !isWebDesktop; bool _showGestureHelp = false; String _value = ''; Orientation? _currentOrientation; + double _viewInsetsBottom = 0; + + Timer? _timerDidChangeMetrics; final _blockableOverlayState = BlockableOverlayState(); @@ -89,10 +106,21 @@ class _RemotePageState extends State { 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); @@ -104,6 +132,7 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); + _timerDidChangeMetrics?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -118,6 +147,39 @@ class _RemotePageState extends State { gFFI.chatModel.onVoiceCallClosed("End connetion"); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + trySyncClipboard(); + } + } + + // For client side + // When swithing from other app to this app, try to sync clipboard. + void trySyncClipboard() { + gFFI.invokeMethod("try_sync_clipboard"); + } + + @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. @@ -155,9 +217,9 @@ class _RemotePageState extends State { var oldValue = _value; _value = newValue; var i = newValue.length - 1; - for (; i >= 0 && newValue[i] != '\1'; --i) {} + for (; i >= 0 && newValue[i] != '1'; --i) {} var j = oldValue.length - 1; - for (; j >= 0 && oldValue[j] != '\1'; --j) {} + for (; j >= 0 && oldValue[j] != '1'; --j) {} if (i < j) j = i; var subNewValue = newValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1); @@ -206,8 +268,8 @@ class _RemotePageState extends State { _value = newValue; if (oldValue.isNotEmpty && newValue.isNotEmpty && - oldValue[0] == '\1' && - newValue[0] != '\1') { + oldValue[0] == '1' && + newValue[0] != '1') { // clipboard oldValue = ''; } @@ -511,7 +573,9 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), + KeyHelpTools( + keyboardIsVisible: keyboardIsVisible, + showGestureHelp: _showGestureHelp), SizedBox( width: 0, height: 0, @@ -531,8 +595,16 @@ class _RemotePageState extends State { 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: handleSoftKeyboardInput, - ), + ).workaroundFreezeLinuxMint(), ), ]; if (showCursorPaint) { @@ -740,10 +812,14 @@ class _RemotePageState extends State { } class KeyHelpTools extends StatefulWidget { + final bool keyboardIsVisible; + final bool showGestureHelp; + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] - final bool requestShow; + bool get requestShow => keyboardIsVisible || showGestureHelp; - KeyHelpTools({required this.requestShow}); + KeyHelpTools( + {required this.keyboardIsVisible, required this.showGestureHelp}); @override State createState() => _KeyHelpToolsState(); @@ -788,7 +864,8 @@ class _KeyHelpToolsState extends State { final size = renderObject.size; Offset pos = renderObject.localToGlobal(Offset.zero); gFFI.cursorModel.keyHelpToolsVisibilityChanged( - Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height)); + Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height), + widget.keyboardIsVisible); } } @@ -800,13 +877,16 @@ class _KeyHelpToolsState extends State { inputModel.command; if (!_pin && !hasModifierOn && !widget.requestShow) { - gFFI.cursorModel.keyHelpToolsVisibilityChanged(null); + gFFI.cursorModel + .keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible); return Offstage(); } final size = MediaQuery.of(context).size; final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; + final isWin = pi.platform == kPeerPlatformWindows; + final isLinux = pi.platform == kPeerPlatformLinux; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); @@ -887,6 +967,28 @@ class _KeyHelpToolsState extends State { wrap('PgDn', () { inputModel.inputKey('VK_NEXT'); }), + // to-do: support PrtScr on Mac + if (isWin || isLinux) + wrap('PrtScr', () { + inputModel.inputKey('VK_SNAPSHOT'); + }), + if (isWin || isLinux) + wrap('ScrollLock', () { + inputModel.inputKey('VK_SCROLL'); + }), + if (isWin || isLinux) + wrap('Pause', () { + inputModel.inputKey('VK_PAUSE'); + }), + if (isWin || isLinux) + // Maybe it's better to call it "Menu" + // https://en.wikipedia.org/wiki/Menu_key + wrap('Menu', () { + inputModel.inputKey('Apps'); + }), + wrap('Enter', () { + inputModel.inputKey('VK_ENTER'); + }), SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); @@ -937,11 +1039,11 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = gFFI.cursorModel.adjustForKeyboard(); 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), + image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s), ); } } @@ -955,7 +1057,6 @@ class CursorPaint extends StatelessWidget { final m = Provider.of(context); final c = Provider.of(context); final ffiModel = Provider.of(context); - final adjust = gFFI.cursorModel.adjustForKeyboard(); final s = c.scale; double hotx = m.hotx; double hoty = m.hoty; @@ -987,11 +1088,12 @@ class CursorPaint extends StatelessWidget { factor = s / mins; } final s2 = s < mins ? mins : s; + final adjust = c.getAdjustY(); return CustomPaint( painter: ImagePainter( image: image, x: (m.x - hotx) * factor + c.x / s2, - y: (m.y - hoty) * factor + (c.y - adjust) / s2, + y: (m.y - hoty) * factor + (c.y + adjust) / s2, scale: s2), ); } @@ -1194,7 +1296,9 @@ void showOptions( toggles + [privacyModeWidget]), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); } TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { @@ -1213,7 +1317,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } @@ -1255,7 +1361,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 527e7446ff20..e92400dba6da 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget { class _ScanPageState extends State { QRViewController? controller; final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + StreamSubscription? scanSubscription; - // In order to get hot reload to work we need to pause the camera if the platform - // is android, or resume the camera if the platform is iOS. @override void reassemble() { super.reassemble(); - if (isAndroid) { + if (isAndroid && controller != null) { controller!.pauseCamera(); + } else if (controller != null) { + controller!.resumeCamera(); } - controller!.resumeCamera(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Scan QR'), - actions: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.image_search), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? file = - await picker.pickImage(source: ImageSource.gallery); - if (file != null) { - var image = img.decodeNamedImage( - file.path, File(file.path).readAsBytesSync())!; - - LuminanceSource source = RGBLuminanceSource( - image.width, - image.height, - image - .getBytes(order: img.ChannelOrder.abgr) - .buffer - .asInt32List()); - var bitmap = BinaryBitmap(HybridBinarizer(source)); - - var reader = QRCodeReader(); - try { - var result = reader.decode(bitmap); - if (result.text.startsWith(bind.mainUriPrefixSync())) { - handleUriLink(uriString: result.text); - } else { - showServerSettingFromQr(result.text); - } - } catch (e) { - showToast('No QR code found'); - } - } - }), - IconButton( - color: Colors.yellow, - icon: Icon(Icons.flash_on), - iconSize: 32.0, - onPressed: () async { - await controller?.toggleFlash(); - }), - IconButton( - color: Colors.white, - icon: Icon(Icons.switch_camera), - iconSize: 32.0, - onPressed: () async { - await controller?.flipCamera(); - }, - ), - ], - ), - body: _buildQrView(context)); + appBar: AppBar( + title: const Text('Scan QR'), + actions: [ + _buildImagePickerButton(), + _buildFlashToggleButton(), + _buildCameraSwitchButton(), + ], + ), + body: _buildQrView(context), + ); } Widget _buildQrView(BuildContext context) { - // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. - var scanArea = (MediaQuery.of(context).size.width < 400 || - MediaQuery.of(context).size.height < 400) + var scanArea = MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400 ? 150.0 : 300.0; - // To ensure the Scanner view is properly sizes after rotation - // we need to listen for Flutter SizeChanged notification and update controller return QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: scanArea), + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), ); } @@ -116,7 +69,7 @@ class _ScanPageState extends State { setState(() { this.controller = controller; }); - controller.scannedDataStream.listen((scanData) { + scanSubscription = controller.scannedDataStream.listen((scanData) { if (scanData.code != null) { showServerSettingFromQr(scanData.code!); } @@ -129,8 +82,66 @@ class _ScanPageState extends State { } } + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? file = await picker.pickImage(source: ImageSource.gallery); + if (file != null) { + try { + var image = img.decodeImage(await File(file.path).readAsBytes())!; + LuminanceSource source = RGBLuminanceSource( + image.width, + image.height, + image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(), + ); + var bitmap = BinaryBitmap(HybridBinarizer(source)); + + var reader = QRCodeReader(); + var result = reader.decode(bitmap); + if (result.text.startsWith(bind.mainUriPrefixSync())) { + handleUriLink(uriString: result.text); + } else { + showServerSettingFromQr(result.text); + } + } catch (e) { + showToast('No QR code found'); + } + } + } + + Widget _buildImagePickerButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.image_search), + iconSize: 32.0, + onPressed: _pickImage, + ); + } + + Widget _buildFlashToggleButton() { + return IconButton( + color: Colors.yellow, + icon: Icon(Icons.flash_on), + iconSize: 32.0, + onPressed: () async { + await controller?.toggleFlash(); + }, + ); + } + + Widget _buildCameraSwitchButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.switch_camera), + iconSize: 32.0, + onPressed: () async { + await controller?.flipCamera(); + }, + ); + } + @override void dispose() { + scanSubscription?.cancel(); controller?.dispose(); super.dispose(); } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 06258e5a5ae0..db91e998b6ea 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final isPermanent = model.verificationMethod == kUsePermanentPassword; final serverModel = Provider.of(context); const Color colorPositive = Colors.green; @@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget { } } + final showOneTime = serverModel.approveMode != 'click' && + serverModel.verificationMethod != kUsePermanentPassword; return PaddingCard( title: translate('Your Device'), child: Column( @@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget { ]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - isPermanent ? '-' : model.serverPasswd.value.text, + !showOneTime ? '-' : model.serverPasswd.value.text, style: textStyleValue, ), - isPermanent + !showOneTime ? SizedBox.shrink() : Row(children: [ IconButton( @@ -595,7 +596,9 @@ class _PermissionCheckerState extends State { translate("android_version_audio_tip"), style: const TextStyle(color: MyTheme.darkGray), )) - ]) + ]), + PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, + serverModel.toggleClipboard), ])); } } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c817fda4e514..83cfa2fb23a0 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:convert'; +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'; @@ -69,6 +71,7 @@ class _SettingsState extends State with WidgetsBindingObserver { false; //androidVersion >= 26; // remove because not work on every device var _ignoreBatteryOpt = false; var _enableStartOnBoot = false; + var _checkUpdateOnStartup = false; var _floatingWindowDisabled = false; var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window var _enableAbr = false; @@ -78,6 +81,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _enableRecordSession = false; var _enableHardwareCodec = false; var _autoRecordIncomingSession = false; + var _autoRecordOutgoingSession = false; var _allowAutoDisconnect = false; var _localIP = ""; var _directAccessPort = ""; @@ -87,6 +91,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _hideServer = false; var _hideProxy = false; var _hideNetwork = false; + var _enableTrustedDevices = false; _SettingsState() { _enableAbr = option2bool( @@ -102,6 +107,8 @@ class _SettingsState extends State with WidgetsBindingObserver { bind.mainGetOptionSync(key: kOptionEnableHwcodec)); _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); + _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing, + bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing)); _localIP = bind.mainGetOptionSync(key: 'local-ip-addr'); _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort); _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect, @@ -113,6 +120,7 @@ class _SettingsState extends State with WidgetsBindingObserver { _hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; _hideNetwork = bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y'; + _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices); } @override @@ -148,6 +156,13 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableStartOnBoot = enableStartOnBoot; } + var checkUpdateOnStartup = + mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate); + if (checkUpdateOnStartup != _checkUpdateOnStartup) { + update = true; + _checkUpdateOnStartup = checkUpdateOnStartup; + } + var floatingWindowDisabled = bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" || !await AndroidPermissionManager.check(kSystemAlertWindow); @@ -228,6 +243,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final outgoingOnly = bind.isOutgoingOnly(); + final incommingOnly = bind.isIncomingOnly(); final customClientSection = CustomSettingsSection( child: Column( children: [ @@ -243,18 +259,76 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); final List enhancementsTiles = []; - final List shareScreenTiles = [ + final enable2fa = bind.mainHasValid2FaSync(); + final List tfaTiles = [ SettingsTile.switchTile( title: Text(translate('enable-2fa-title')), - initialValue: bind.mainHasValid2FaSync(), - onToggle: (_) async { + initialValue: enable2fa, + onToggle: (v) async { update() async { setState(() {}); } - change2fa(callback: update); + if (v == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () { + change2fa(callback: update); + }); + } else { + change2fa(callback: update); + } }, ), + if (enable2fa) + SettingsTile.switchTile( + title: Text(translate('Telegram bot')), + initialValue: bind.mainHasValidBotSync(), + onToggle: (v) async { + update() async { + setState(() {}); + } + + if (v == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () { + changeBot(callback: update); + }); + } else { + changeBot(callback: update); + } + }, + ), + if (enable2fa) + SettingsTile.switchTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate('Enable trusted devices')), + Text('* ${translate('enable-trusted-devices-tip')}', + style: Theme.of(context).textTheme.bodySmall), + ], + ), + initialValue: _enableTrustedDevices, + onToggle: isOptionFixed(kOptionEnableTrustedDevices) + ? null + : (v) async { + mainSetBoolOption(kOptionEnableTrustedDevices, v); + setState(() { + _enableTrustedDevices = v; + }); + }, + ), + if (enable2fa && _enableTrustedDevices) + SettingsTile( + title: Text(translate('Manage trusted devices')), + trailing: Icon(Icons.arrow_forward_ios), + onPressed: (context) { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return _ManageTrustedDevices(); + })); + }) + ]; + final List shareScreenTiles = [ SettingsTile.switchTile( title: Text(translate('Deny LAN discovery')), initialValue: _denyLANDiscovery, @@ -487,6 +561,22 @@ class _SettingsState extends State with WidgetsBindingObserver { gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); })); + if (!bind.isCustomClient()) { + enhancementsTiles.add( + SettingsTile.switchTile( + initialValue: _checkUpdateOnStartup, + title: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(translate('Check for software update on startup')), + ]), + onToggle: (bool toValue) async { + await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue); + setState(() => _checkUpdateOnStartup = toValue); + }, + ), + ); + } + onFloatingWindowChanged(bool toValue) async { if (toValue) { if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { @@ -613,35 +703,63 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ), ]), - if (isAndroid && !outgoingOnly) + if (isAndroid) SettingsSection( title: Text(translate("Recording")), tiles: [ - SettingsTile.switchTile( - title: - Text(translate('Automatically record incoming sessions')), - leading: Icon(Icons.videocam), - description: Text( - "${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"), - initialValue: _autoRecordIncomingSession, - onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) - ? null - : (v) async { - await bind.mainSetOption( - key: kOptionAllowAutoRecordIncoming, - value: - bool2option(kOptionAllowAutoRecordIncoming, v)); - final newValue = option2bool( - kOptionAllowAutoRecordIncoming, - await bind.mainGetOption( - key: kOptionAllowAutoRecordIncoming)); - setState(() { - _autoRecordIncomingSession = newValue; - }); - }, + if (!outgoingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record incoming sessions')), + initialValue: _autoRecordIncomingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) + ? null + : (v) async { + await bind.mainSetOption( + key: kOptionAllowAutoRecordIncoming, + value: bool2option( + kOptionAllowAutoRecordIncoming, v)); + final newValue = option2bool( + kOptionAllowAutoRecordIncoming, + await bind.mainGetOption( + key: kOptionAllowAutoRecordIncoming)); + setState(() { + _autoRecordIncomingSession = newValue; + }); + }, + ), + if (!incommingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record outgoing sessions')), + initialValue: _autoRecordOutgoingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing) + ? null + : (v) async { + await bind.mainSetLocalOption( + key: kOptionAllowAutoRecordOutgoing, + value: bool2option( + kOptionAllowAutoRecordOutgoing, v)); + final newValue = option2bool( + kOptionAllowAutoRecordOutgoing, + bind.mainGetLocalOption( + key: kOptionAllowAutoRecordOutgoing)); + setState(() { + _autoRecordOutgoingSession = newValue; + }); + }, + ), + SettingsTile( + title: Text(translate("Directory")), + description: Text(bind.mainVideoSaveDirectory(root: false)), ), ], ), + if (isAndroid && + !disabledSettings && + !outgoingOnly && + !hideSecuritySettings) + SettingsSection(title: Text('2FA'), tiles: tfaTiles), if (isAndroid && !disabledSettings && !outgoingOnly && @@ -664,9 +782,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( @@ -735,11 +851,6 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings(OverlayDialogManager dialogManager) async { - Map options = jsonDecode(await bind.mainGetOptions()); - showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); -} - void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; @@ -815,9 +926,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), @@ -963,6 +1072,51 @@ class __DisplayPageState extends State<_DisplayPage> { } } +class _ManageTrustedDevices extends StatefulWidget { + const _ManageTrustedDevices(); + + @override + State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState(); +} + +class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> { + RxList trustedDevices = RxList.empty(growable: true); + RxList selectedDevices = RxList.empty(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(translate('Manage trusted devices')), + centerTitle: true, + actions: [ + Obx(() => IconButton( + icon: Icon(Icons.delete, color: Colors.white), + onPressed: selectedDevices.isEmpty + ? null + : () { + confrimDeleteTrustedDevicesDialog( + trustedDevices, selectedDevices); + })) + ], + ), + body: FutureBuilder( + future: TrustedDevice.get(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + final devices = snapshot.data as List; + trustedDevices = devices.obs; + return trustedDevicesTable(trustedDevices, selectedDevices); + }), + ); + } +} + class _RadioEntry { final String label; final String value; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 391bec669d58..ebedd79d44fe 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; @@ -65,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('Too short, at least 6 characters.'); }, - ), + ).workaroundFreezeLinuxMint(), TextFormField( obscureText: true, keyboardType: TextInputType.visiblePassword, @@ -84,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('The confirmation is not identical.'); }, - ), + ).workaroundFreezeLinuxMint(), ])), onCancel: close, onSubmit: (validateLength && validateSame) ? submit : null, @@ -146,6 +147,16 @@ void setTemporaryPasswordLengthDialog( }, backDismiss: true, clickMaskDismiss: true); } +void showServerSettings(OverlayDialogManager dialogManager) async { + Map options = {}; + try { + options = jsonDecode(await bind.mainGetOptions()); + } catch (e) { + print("Invalid server config: $e"); + } + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); +} + void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { var isInProgress = false; @@ -184,6 +195,43 @@ void showServerSettingsWithValue( return ret; } + Widget buildField( + String label, TextEditingController controller, String errorMsg, + {String? Function(String?)? validator, bool autofocus = false}) { + if (isDesktop || isWeb) { + return Row( + children: [ + SizedBox( + width: 120, + child: Text(label), + ), + SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + errorText: errorMsg.isEmpty ? null : errorMsg, + contentPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 12), + ), + validator: validator, + autofocus: autofocus, + ).workaroundFreezeLinuxMint(), + ), + ], + ); + } + + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: label, + errorText: errorMsg.isEmpty ? null : errorMsg, + ), + validator: validator, + ).workaroundFreezeLinuxMint(); + } + return CustomAlertDialog( title: Row( children: [ @@ -191,55 +239,45 @@ void showServerSettingsWithValue( ...ServerConfigImportExportWidgets(controllers, errMsgs), ], ), - content: Form( + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Form( child: Obx(() => Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idCtrl, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg.value.isEmpty - ? null - : idServerMsg.value), - ) - ] + - [ - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg.value.isEmpty - ? null - : relayServerMsg.value), - ) - ] + - [ - TextFormField( - controller: apiCtrl, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (v) { - if (v != null && v.isNotEmpty) { - if (!(v.startsWith('http://') || - v.startsWith("https://"))) { - return translate("invalid_http"); - } + mainAxisSize: MainAxisSize.min, + children: [ + buildField(translate('ID Server'), idCtrl, idServerMsg.value, + autofocus: true), + SizedBox(height: 8), + if (!isIOS && !isWeb) ...[ + buildField(translate('Relay Server'), relayCtrl, + relayServerMsg.value), + SizedBox(height: 8), + ], + buildField( + translate('API Server'), + apiCtrl, + apiServerMsg.value, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); } - return null; - }, - ), - TextFormField( - controller: keyCtrl, - decoration: InputDecoration( - labelText: 'Key', - ), + } + return null; + }, + ), + SizedBox(height: 8), + buildField('Key', keyCtrl, ''), + if (isInProgress) + Padding( + padding: EdgeInsets.only(top: 8), + child: LinearProgressIndicator(), ), - // NOT use Offstage to wrap LinearProgressIndicator - if (isInProgress) const LinearProgressIndicator(), - ]))), + ], + )), + ), + ), actions: [ dialogButton('Cancel', onPressed: () { close(); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 8cd5cc92293d..3aa722a5abf1 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -33,6 +33,8 @@ bool filterAbTagByIntersection() { const _personalAddressBookName = "My address book"; const _legacyAddressBookName = "Legacy address book"; +const kUntagged = "Untagged"; + enum ForcePullAb { listAndCurrent, current, @@ -66,10 +68,16 @@ class AbModel { var listInitialized = false; var _maxPeerOneAb = 0; + late final Peers peersModel; + WeakReference parent; AbModel(this.parent) { addressbooks.clear(); + peersModel = Peers( + name: PeersModelName.addressBook, + getInitPeers: () => currentAbPeers, + loadEvent: LoadEvent.addressBook); if (desktopType == DesktopType.main) { Timer.periodic(Duration(milliseconds: 500), (timer) async { if (_timerCounter++ % 6 == 0) { @@ -111,9 +119,10 @@ class AbModel { Future _pullAb( {required ForcePullAb? force, required bool quiet}) async { if (bind.isDisableAb()) return; - debugPrint("pullAb, force: $force, quiet: $quiet"); if (!gFFI.userModel.isLogin) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (force == null && listInitialized && current.initialized) return; + debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { try { // Read personal guid every time to avoid upgrading the server without closing the main window @@ -417,6 +426,7 @@ class AbModel { // #region tags Future addTags(List tagList) async { + tagList.removeWhere((e) => e == kUntagged); final ret = await current.addTags(tagList, {}); await pullNonLegacyAfterChange(); _saveCache(); @@ -638,6 +648,9 @@ class AbModel { } Color getCurrentAbTagColor(String tag) { + if (tag == kUntagged) { + return MyTheme.accent; + } int? colorValue = current.tagColors[tag]; if (colorValue != null) { return Color(colorValue); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 778c94357042..d79c9f070222 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier { } } - _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || - chatWindowOverlayEntry == null); + _isChatOverlayHide() => + ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) || + chatWindowOverlayEntry == null); toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); - if (!isDesktop) { + if (!(isDesktop || isWebDesktop)) { showChatIconOverlay(); } showChatWindowOverlay(chatInitPos: chatInitPos); diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart index ce9b711a2782..6609f1191c3c 100644 --- a/flutter/lib/models/cm_file_model.dart +++ b/flutter/lib/models/cm_file_model.dart @@ -33,6 +33,8 @@ class CmFileModel { _onFileRemove(evt['remove']); } else if (evt['create_dir'] != null) { _onDirCreate(evt['create_dir']); + } else if (evt['rename'] != null) { + _onRename(evt['rename']); } } @@ -59,8 +61,6 @@ class CmFileModel { _dealOneJob(dynamic l, bool calcSpeed) { final data = TransferJobSerdeData.fromJson(l); - Client? client = - gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId); var jobTable = _jobTables[data.connId]; if (jobTable == null) { debugPrint("jobTable should not be null"); @@ -70,12 +70,7 @@ class CmFileModel { if (job == null) { job = CmFileLog(); jobTable.add(job); - final currentSelectedTab = - gFFI.serverModel.tabController.state.value.selectedTabInfo; - if (!(gFFI.chatModel.isShowCMSidePage && - currentSelectedTab.key == data.connId.toString())) { - client?.unreadChatMessageCount.value += 1; - } + _addUnread(data.connId); } job.id = data.id; job.action = @@ -167,8 +162,6 @@ class CmFileModel { try { dynamic d = jsonDecode(log); FileActionLog data = FileActionLog.fromJson(d); - Client? client = - gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId); var jobTable = _jobTables[data.connId]; if (jobTable == null) { debugPrint("jobTable should not be null"); @@ -179,17 +172,45 @@ class CmFileModel { ..fileName = data.path ..action = CmFileAction.createDir ..state = JobState.done); - final currentSelectedTab = - gFFI.serverModel.tabController.state.value.selectedTabInfo; - if (!(gFFI.chatModel.isShowCMSidePage && - currentSelectedTab.key == data.connId.toString())) { - client?.unreadChatMessageCount.value += 1; + _addUnread(data.connId); + jobTable.refresh(); + } catch (e) { + debugPrint('$e'); + } + } + + _onRename(dynamic log) { + try { + dynamic d = jsonDecode(log); + FileRenamenLog data = FileRenamenLog.fromJson(d); + var jobTable = _jobTables[data.connId]; + if (jobTable == null) { + debugPrint("jobTable should not be null"); + return; } + final fileName = '${data.path} -> ${data.newName}'; + jobTable.add(CmFileLog() + ..id = 0 + ..fileName = fileName + ..action = CmFileAction.rename + ..state = JobState.done); + _addUnread(data.connId); jobTable.refresh(); } catch (e) { debugPrint('$e'); } } + + _addUnread(int connId) { + Client? client = + gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId); + final currentSelectedTab = + gFFI.serverModel.tabController.state.value.selectedTabInfo; + if (!(gFFI.chatModel.isShowCMSidePage && + currentSelectedTab.key == connId.toString())) { + client?.unreadChatMessageCount.value += 1; + } + } } enum CmFileAction { @@ -198,6 +219,7 @@ enum CmFileAction { localToRemote, remove, createDir, + rename, } class CmFileLog { @@ -285,3 +307,22 @@ class FileActionLog { dir: d['dir'] ?? false, ); } + +class FileRenamenLog { + int connId = 0; + String path = ''; + String newName = ''; + + FileRenamenLog({ + required this.connId, + required this.path, + required this.newName, + }); + + FileRenamenLog.fromJson(dynamic d) + : this( + connId: d['connId'] ?? 0, + path: d['path'] ?? '', + newName: d['newName'] ?? '', + ); +} diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index ab8df3c4539e..c6cf55256de8 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -181,6 +181,7 @@ class TextureModel { } updateCurrentDisplay(int curDisplay) { + if (isWeb) return; final ffi = parent.target; if (ffi == null) return; tryCreateTexture(int idx) { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c32ec54056ed..4a00b803e340 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -3,9 +3,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; import '../consts.dart'; import 'model.dart'; @@ -33,6 +36,7 @@ class JobID { } typedef GetSessionID = SessionID Function(); +typedef GetDialogManager = OverlayDialogManager? Function(); class FileModel { final WeakReference parent; @@ -44,13 +48,15 @@ class FileModel { late final FileController remoteController; late final GetSessionID getSessionID; + late final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); late final FileDialogEventLoop evtLoop; FileModel(this.parent) { getSessionID = () => parent.target!.sessionId; + getDialogManager = () => parent.target?.dialogManager; fileFetcher = FileFetcher(getSessionID); - jobController = JobController(getSessionID); + jobController = JobController(getSessionID, getDialogManager); localController = FileController( isLocal: true, getSessionID: getSessionID, @@ -70,7 +76,7 @@ class FileModel { Future onReady() async { await evtLoop.onReady(); - await localController.onReady(); + if (!isWeb) await localController.onReady(); await remoteController.onReady(); } @@ -82,7 +88,7 @@ class FileModel { } Future refreshAll() async { - await localController.refresh(); + if (!isWeb) await localController.refresh(); await remoteController.refresh(); } @@ -94,6 +100,10 @@ class FileModel { fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); } + void receiveEmptyDirs(Map evt) { + fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']); + } + Future postOverrideFileConfirm(Map evt) async { evtLoop.pushEvent( _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); @@ -224,6 +234,54 @@ class FileModel { ); }, useAnimation: false); } + + void onSelectedFiles(dynamic obj) { + localController.selectedItems.clear(); + + try { + int handleIndex = int.parse(obj['handleIndex']); + final file = jsonDecode(obj['file']); + var entry = Entry.fromJson(file); + entry.path = entry.name; + final otherSideData = remoteController.directoryData(); + final toPath = otherSideData.directory.path; + final isWindows = otherSideData.options.isWindows; + final showHidden = otherSideData.options.showHidden; + final jobID = jobController.addTransferJob(entry, false); + webSendLocalFiles( + handleIndex: handleIndex, + actId: jobID, + path: entry.path, + to: PathUtil.join(toPath, entry.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: false, + ); + } catch (e) { + debugPrint("Failed to decode onSelectedFiles: $e"); + } + } + + void sendEmptyDirs(dynamic obj) { + late final List emptyDirs; + try { + emptyDirs = jsonDecode(obj['dirs'] as String); + } catch (e) { + debugPrint("Failed to decode sendEmptyDirs: $e"); + } + final otherSideData = remoteController.directoryData(); + final toPath = otherSideData.directory.path; + final isPeerWindows = otherSideData.options.isWindows; + + final isLocalWindows = isWindows || isWebOnWindows; + for (var dir in emptyDirs) { + if (isLocalWindows != isPeerWindows) { + dir = PathUtil.convert(dir, isLocalWindows, isPeerWindows); + } + var peerPath = PathUtil.join(toPath, dir, isPeerWindows); + remoteController.createDirWithRemote(peerPath, true); + } + } } class DirectoryData { @@ -437,7 +495,8 @@ class FileController { } /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems). - void sendFiles(SelectedItems items, DirectoryData otherSideData) { + Future sendFiles( + SelectedItems items, DirectoryData otherSideData) async { /// ignore wrong items side status if (items.isLocal != isLocal) { return; @@ -450,7 +509,7 @@ class FileController { final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; for (var from in items.items) { - final jobID = jobController.add(from, isRemoteToLocal); + final jobID = jobController.addTransferJob(from, isRemoteToLocal); bind.sessionSendFiles( sessionId: sessionId, actId: jobID, @@ -458,10 +517,48 @@ class FileController { to: PathUtil.join(toPath, from.name, isWindows), fileNum: 0, includeHidden: showHidden, - isRemote: isRemoteToLocal); + isRemote: isRemoteToLocal, + isDir: from.isDirectory); debugPrint( "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } + + if (isWeb || + (!isLocal && + versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0)) { + return; + } + + final List entrys = items.items.toList(); + var isRemote = isLocal == true ? true : false; + + await Future.forEach(entrys, (Entry item) async { + if (!item.isDirectory) { + return; + } + + final List paths = []; + + final emptyDirs = + await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden); + + if (emptyDirs.isEmpty) { + return; + } else { + for (var dir in emptyDirs) { + paths.add(dir.path); + } + } + + final dirs = paths.map((path) { + return PathUtil.getOtherSidePath(directory.value.path, path, + options.value.isWindows, toPath, isWindows); + }); + + for (var dir in dirs) { + createDirWithRemote(dir, isRemote); + } + }); } bool _removeCheckboxRemember = false; @@ -485,7 +582,7 @@ class FileController { } else if (item.isDirectory) { title = translate("Not an empty directory"); dialogManager?.showLoading(translate("Waiting")); - final fd = await fileFetcher.fetchDirectoryRecursive( + final fd = await fileFetcher.fetchDirectoryRecursiveToRemove( jobID, item.path, items.isLocal, true); if (fd.path.isEmpty) { fd.path = item.path; @@ -493,13 +590,21 @@ class FileController { fd.format(isWindows); dialogManager?.dismissAll(); if (fd.entries.isEmpty) { + var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0); final confirm = await showRemoveDialog( translate( "Are you sure you want to delete this empty directory?"), item.name, false); if (confirm == true) { - sendRemoveEmptyDir(item.path, 0); + sendRemoveEmptyDir( + item.path, + 0, + deleteJobId, + ); + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", state: JobState.done); } return; } @@ -507,6 +612,13 @@ class FileController { } else { entries = []; } + int deleteJobId; + if (item.isDirectory) { + deleteJobId = + jobController.addDeleteDirJob(item, !isLocal, entries.length); + } else { + deleteJobId = jobController.addDeleteFileJob(item, !isLocal); + } for (var i = 0; i < entries.length; i++) { final dirShow = item.isDirectory @@ -521,24 +633,32 @@ class FileController { ); try { if (confirm == true) { - sendRemoveFile(entries[i].path, i); + sendRemoveFile(entries[i].path, i, deleteJobId); final res = await jobController.jobResultListener.start(); // handle remove res; if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } + } else { + jobController.updateJobStatus(deleteJobId, + file_num: i, error: "cancel"); } if (_removeCheckboxRemember) { if (confirm == true) { for (var j = i + 1; j < entries.length; j++) { - sendRemoveFile(entries[j].path, j); + sendRemoveFile(entries[j].path, j, deleteJobId); final res = await jobController.jobResultListener.start(); if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } } + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", + file_num: entries.length, + state: JobState.done); } break; } @@ -617,54 +737,128 @@ class FileController { }, useAnimation: false); } - void sendRemoveFile(String path, int fileNum) { + void sendRemoveFile(String path, int fileNum, int actId) { bind.sessionRemoveFile( sessionId: sessionId, - actId: JobController.jobID.next(), + actId: actId, path: path, isRemote: !isLocal, fileNum: fileNum); } - void sendRemoveEmptyDir(String path, int fileNum) { + void sendRemoveEmptyDir(String path, int fileNum, int actId) { history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( - sessionId: sessionId, - actId: JobController.jobID.next(), - path: path, - isRemote: !isLocal); + sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } - Future createDir(String path) async { + Future createDirWithRemote(String path, bool isRemote) async { bind.sessionCreateDir( sessionId: sessionId, actId: JobController.jobID.next(), path: path, - isRemote: !isLocal); + isRemote: isRemote); + } + + Future createDir(String path) async { + await createDirWithRemote(path, !isLocal); + } + + Future renameAction(Entry item, bool isLocal) async { + final textEditingController = TextEditingController(text: item.name); + String? errorText; + dialogManager?.show((setState, close, context) { + textEditingController.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); + submit() async { + final newName = textEditingController.text; + if (newName.isEmpty || newName == item.name) { + close(); + return; + } + if (directory.value.entries.any((e) => e.name == newName)) { + setState(() { + errorText = translate("Already exists"); + }); + return; + } + if (!PathUtil.validName(newName, options.value.isWindows)) { + setState(() { + if (item.isDirectory) { + errorText = translate("Invalid folder name"); + } else { + errorText = translate("Invalid file name"); + } + }); + return; + } + await bind.sessionRenameFile( + sessionId: sessionId, + actId: JobController.jobID.next(), + path: item.path, + newName: newName, + isRemote: !isLocal); + close(); + } + + return CustomAlertDialog( + content: Column( + children: [ + DialogTextField( + title: '${translate('Rename')} ${item.name}', + controller: textEditingController, + errorText: errorText, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); } } +const _kOneWayFileTransferError = 'one-way-file-transfer-tip'; + class JobController { static final JobID jobID = JobID(); final jobTable = List.empty(growable: true).obs; final jobResultListener = JobResultListener>(); final GetSessionID getSessionID; + final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); + OverlayDialogManager? get alogManager => getDialogManager(); + int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; - JobController(this.getSessionID); + JobController(this.getSessionID, this.getDialogManager); int getJob(int id) { return jobTable.indexWhere((element) => element.id == id); } - // JobProgress? getJob(int id) { - // return jobTable.firstWhere((element) => element.id == id); - // } - // return jobID - int add(Entry from, bool isRemoteToLocal) { + int addTransferJob(Entry from, bool isRemoteToLocal) { final jobID = JobController.jobID.next(); jobTable.add(JobProgress() + ..type = JobType.transfer ..fileName = path.basename(from.path) ..jobName = from.path ..totalSize = from.size @@ -674,6 +868,33 @@ class JobController { return jobID; } + int addDeleteFileJob(Entry file, bool isRemote) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteFile + ..fileName = path.basename(file.path) + ..jobName = file.path + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + + int addDeleteDirJob(Entry file, bool isRemote, int fileCount) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteDir + ..fileName = path.basename(file.path) + ..jobName = file.path + ..fileCount = fileCount + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + void tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); @@ -684,7 +905,7 @@ class JobController { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); - debugPrint("update job $id with $evt"); + job.recvJobRes = true; jobTable.refresh(); } } catch (e) { @@ -692,20 +913,48 @@ class JobController { } } - void jobDone(Map evt) async { + Future jobDone(Map evt) async { if (jobResultListener.isListening) { jobResultListener.complete(evt); - return; + // return; } - - int id = int.parse(evt['id']); + int id = -1; + int? fileNum = 0; + double? speed = 0; + try { + id = int.parse(evt['id']); + } catch (_) {} final jobIndex = getJob(id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.finishedSize = job.totalSize; + if (jobIndex == -1) return true; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (job.type == JobType.deleteFile) { + job.state = JobState.done; + } else if (job.type == JobType.deleteDir) { + try { + fileNum = int.tryParse(evt['file_num']); + } catch (_) {} + if (fileNum != null) { + if (fileNum < job.fileNum) return true; // file_num can be 0 at last + job.fileNum = fileNum; + if (fileNum >= job.fileCount - 1) { + job.state = JobState.done; + } + } + } else { + try { + fileNum = int.tryParse(evt['file_num']); + speed = double.tryParse(evt['speed']); + } catch (_) {} + if (fileNum != null) job.fileNum = fileNum; + if (speed != null) job.speed = speed; job.state = JobState.done; - job.fileNum = int.parse(evt['file_num']); - jobTable.refresh(); + } + jobTable.refresh(); + if (job.type == JobType.deleteDir) { + return job.state == JobState.done; + } else { + return true; } } @@ -716,16 +965,61 @@ class JobController { final job = jobTable[jobIndex]; job.state = JobState.error; job.err = err; - job.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - job.state = JobState.done; - job.finishedSize = job.totalSize; + job.recvJobRes = true; + if (job.type == JobType.transfer) { + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } + } else if (job.type == JobType.deleteDir) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + } else if (job.type == JobType.deleteFile) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } } jobTable.refresh(); } + if (err == _kOneWayFileTransferError) { + if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { + final dm = alogManager; + if (dm != null) { + _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; + msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm); + } + } + } debugPrint("jobError $evt"); } + void updateJobStatus(int id, + {int? file_num, String? error, JobState? state}) { + final jobIndex = getJob(id); + if (jobIndex < 0) return; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (file_num != null) { + job.fileNum = file_num; + } + if (error != null) { + job.err = error; + job.state = JobState.error; + } + if (state != null) { + job.state = state; + } + if (job.type == JobType.deleteFile && error == null) { + job.state = JobState.done; + } + jobTable.refresh(); + } + Future cancelJob(int id) async { await bind.sessionCancelJob(sessionId: sessionId, actId: id); } @@ -742,6 +1036,7 @@ class JobController { final currJobId = JobController.jobID.next(); String fileName = path.basename(isRemote ? remote : to); var jobProgress = JobProgress() + ..type = JobType.transfer ..fileName = fileName ..jobName = isRemote ? remote : to ..id = currJobId @@ -836,6 +1131,7 @@ class JobResultListener { class FileFetcher { // Map> localTasks = {}; // now we only use read local dir sync Map> remoteTasks = {}; + Map>> remoteEmptyDirsTasks = {}; Map> readRecursiveTasks = {}; final GetSessionID getSessionID; @@ -843,6 +1139,24 @@ class FileFetcher { FileFetcher(this.getSessionID); + Future> registerReadEmptyDirsTask( + bool isLocal, String path) { + // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later + final tasks = remoteEmptyDirsTasks; // bypass now + if (tasks.containsKey(path)) { + throw "Failed to registerReadEmptyDirsTask, already have same read job"; + } + final c = Completer>(); + tasks[path] = c; + + Timer(Duration(seconds: 2), () { + tasks.remove(path); + if (c.isCompleted) return; + c.completeError("Failed to read empty dirs, timeout"); + }); + return c.future; + } + Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later final tasks = remoteTasks; // bypass now @@ -876,6 +1190,25 @@ class FileFetcher { return c.future; } + tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) { + if (msg == null || isLocalStr == null) return; + late final Map>> tasks; + try { + final map = jsonDecode(msg); + final String path = map["path"]; + final List fdJsons = map["empty_dirs"]; + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + + tasks = remoteEmptyDirsTasks; + final completer = tasks.remove(path); + + completer?.complete(fds); + } catch (e) { + debugPrint("tryCompleteJob err: $e"); + } + } + tryCompleteTask(String? msg, String? isLocalStr) { if (msg == null || isLocalStr == null) return; late final Map> tasks; @@ -899,6 +1232,28 @@ class FileFetcher { } } + Future> readEmptyDirs( + String path, bool isLocal, bool showHidden) async { + try { + if (isLocal) { + final res = await bind.sessionReadLocalEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + + final List fdJsons = jsonDecode(res); + + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + return fds; + } else { + await bind.sessionReadRemoteEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + return registerReadEmptyDirsTask(isLocal, path); + } + } catch (e) { + return Future.error(e); + } + } + Future fetchDirectory( String path, bool isLocal, bool showHidden) async { try { @@ -917,11 +1272,11 @@ class FileFetcher { } } - Future fetchDirectoryRecursive( + Future fetchDirectoryRecursiveToRemove( int actID, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - await bind.sessionReadDirRecursive( + await bind.sessionReadDirToRemoveRecursive( sessionId: sessionId, actId: actID, path: path, @@ -1016,8 +1371,12 @@ extension JobStateDisplay on JobState { } } +enum JobType { none, transfer, deleteFile, deleteDir } + class JobProgress { + JobType type = JobType.none; JobState state = JobState.none; + var recvJobRes = false; var id = 0; var fileNum = 0; var speed = 0.0; @@ -1037,7 +1396,9 @@ class JobProgress { int lastTransferredSize = 0; clear() { + type = JobType.none; state = JobState.none; + recvJobRes = false; id = 0; fileNum = 0; speed = 0; @@ -1051,11 +1412,81 @@ class JobProgress { } String display() { - if (state == JobState.done && err == "skipped") { - return translate("Skipped"); + if (type == JobType.transfer) { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + } else if (type == JobType.deleteFile) { + if (err == "cancel") { + return translate("Cancel"); + } } + return state.display(); } + + String getStatus() { + int handledFileCount = recvJobRes ? fileNum + 1 : fileNum; + if (handledFileCount >= fileCount) { + handledFileCount = fileCount; + } + if (state == JobState.done) { + handledFileCount = fileCount; + finishedSize = totalSize; + } + final filesStr = "$handledFileCount/$fileCount files"; + final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : ""; + final sizePercentStr = totalSize > 0 && finishedSize > 0 + ? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}" + : ""; + if (type == JobType.deleteFile) { + return display(); + } else if (type == JobType.deleteDir) { + var res = ''; + if (state == JobState.done || state == JobState.error) { + res = display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += " "; + } + res += filesStr; + } + + if (sizeStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + return res; + } else if (type == JobType.transfer) { + var res = ""; + if (state != JobState.inProgress && state != JobState.none) { + res += display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += filesStr; + } + if (sizeStr.isNotEmpty && state != JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + if (sizePercentStr.isNotEmpty && state == JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizePercentStr; + } + return res; + } + return ''; + } } class _PathStat { @@ -1069,6 +1500,24 @@ class PathUtil { static final windowsContext = path.Context(style: path.Style.windows); static final posixContext = path.Context(style: path.Style.posix); + static String getOtherSidePath(String mainRootPath, String mainPath, + bool isMainWindows, String otherRootPath, bool isOtherWindows) { + final mainPathUtil = isMainWindows ? windowsContext : posixContext; + final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath); + + final names = mainPathUtil.split(relativePath); + + final otherPathUtil = isOtherWindows ? windowsContext : posixContext; + + String path = otherRootPath; + + for (var name in names) { + path = otherPathUtil.join(path, name); + } + + return path; + } + static String join(String path1, String path2, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.join(path1, path2); @@ -1079,10 +1528,23 @@ class PathUtil { return pathUtil.split(path); } + static String convert(String path, bool isMainWindows, bool isOtherWindows) { + final mainPathUtil = isMainWindows ? windowsContext : posixContext; + final otherPathUtil = isOtherWindows ? windowsContext : posixContext; + return otherPathUtil.joinAll(mainPathUtil.split(path)); + } + static String dirname(String path, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.dirname(path); } + + static bool validName(String name, bool isWindows) { + final unixFileNamePattern = RegExp(r'^[^/\0]+$'); + final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$'); + final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern; + return reg.hasMatch(name); + } } class DirectoryOptions { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 50459ffe90eb..b14ccd46b0e0 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -23,11 +23,19 @@ class GroupModel { bool get emtpy => users.isEmpty && peers.isEmpty; - GroupModel(this.parent); + late final Peers peersModel; + + GroupModel(this.parent) { + peersModel = Peers( + name: PeersModelName.group, + getInitPeers: () => peers, + loadEvent: LoadEvent.group); + } Future pull({force = true, quiet = false}) async { if (bind.isDisableGroupPanel()) return; if (!gFFI.userModel.isLogin || groupLoading.value) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (!force && initialized) return; if (!quiet) { groupLoading.value = true; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index dde815789ae2..a30bb79fdbd3 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -177,7 +177,7 @@ class PointerEventToRust { } } -class ToReleaseKeys { +class ToReleaseRawKeys { RawKeyEvent? lastLShiftKeyEvent; RawKeyEvent? lastRShiftKeyEvent; RawKeyEvent? lastLCtrlKeyEvent; @@ -282,6 +282,48 @@ class ToReleaseKeys { } } +class ToReleaseKeys { + KeyEvent? lastLShiftKeyEvent; + KeyEvent? lastRShiftKeyEvent; + KeyEvent? lastLCtrlKeyEvent; + KeyEvent? lastRCtrlKeyEvent; + KeyEvent? lastLAltKeyEvent; + KeyEvent? lastRAltKeyEvent; + KeyEvent? lastLCommandKeyEvent; + KeyEvent? lastRCommandKeyEvent; + KeyEvent? lastSuperKeyEvent; + + reset() { + lastLShiftKeyEvent = null; + lastRShiftKeyEvent = null; + lastLCtrlKeyEvent = null; + lastRCtrlKeyEvent = null; + lastLAltKeyEvent = null; + lastRAltKeyEvent = null; + lastLCommandKeyEvent = null; + lastRCommandKeyEvent = null; + lastSuperKeyEvent = null; + } + + release(KeyEventResult Function(KeyEvent e) handleKeyEvent) { + for (final key in [ + lastLShiftKeyEvent, + lastRShiftKeyEvent, + lastLCtrlKeyEvent, + lastRCtrlKeyEvent, + lastLAltKeyEvent, + lastRAltKeyEvent, + lastLCommandKeyEvent, + lastRCommandKeyEvent, + lastSuperKeyEvent, + ]) { + if (key != null) { + handleKeyEvent(key); + } + } + } +} + class InputModel { final WeakReference parent; String keyboardMode = ''; @@ -292,6 +334,7 @@ class InputModel { var alt = false; var command = false; + final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); // trackpad @@ -339,10 +382,99 @@ class InputModel { } } + void handleKeyDownEventModifiers(KeyEvent e) { + KeyUpEvent upEvent(e) => KeyUpEvent( + physicalKey: e.physicalKey, + logicalKey: e.logicalKey, + timeStamp: e.timeStamp, + ); + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastLCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastRCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastLShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastRShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + if (!command) { + command = true; + } + toReleaseKeys.lastLCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + if (!command) { + command = true; + } + toReleaseKeys.lastRCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + if (!command) { + command = true; + } + toReleaseKeys.lastSuperKeyEvent = upEvent(e); + } + } + + void handleKeyUpEventModifiers(KeyEvent e) { + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + alt = false; + toReleaseKeys.lastLAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + alt = false; + toReleaseKeys.lastRAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + ctrl = false; + toReleaseKeys.lastLCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + ctrl = false; + toReleaseKeys.lastRCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + shift = false; + toReleaseKeys.lastLShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + shift = false; + toReleaseKeys.lastRShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + command = false; + toReleaseKeys.lastLCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + command = false; + toReleaseKeys.lastRCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + command = false; + toReleaseKeys.lastSuperKeyEvent = null; + } + } + KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; - if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { - return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } } final key = e.logicalKey; @@ -358,7 +490,7 @@ class InputModel { command = true; } } - toReleaseKeys.updateKeyDown(key, e); + toReleaseRawKeys.updateKeyDown(key, e); } if (e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || @@ -376,12 +508,82 @@ class InputModel { command = false; } - toReleaseKeys.updateKeyUp(key, e); + toReleaseRawKeys.updateKeyUp(key, e); } // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { - mapKeyboardMode(e); + if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { + mapKeyboardModeRaw(e); + } else { + legacyKeyboardModeRaw(e); + } + + return KeyEventResult.handled; + } + + KeyEventResult handleKeyEvent(KeyEvent e) { + if (isViewOnly) return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } + } + if (isWindows || isLinux) { + // Ignore meta keys. Because flutter window will loose focus if meta key is pressed. + if (e.physicalKey == PhysicalKeyboardKey.metaLeft || + e.physicalKey == PhysicalKeyboardKey.metaRight) { + return KeyEventResult.handled; + } + } + + if (e is KeyUpEvent) { + handleKeyUpEventModifiers(e); + } else if (e is KeyDownEvent) { + handleKeyDownEventModifiers(e); + } + + bool isMobileAndMapMode = false; + if (isMobile) { + // Do not use map mode if mobile -> Android. Android does not support map mode for now. + // Because simulating the physical key events(uhid) which requires root permission is not supported. + if (peerPlatform != kPeerPlatformAndroid) { + if (isIOS) { + isMobileAndMapMode = true; + } else { + // The physicalKey.usbHidUsage may be not correct for soft keyboard on Android. + // iOS does not have this issue. + // 1. Open the soft keyboard on Android + // 2. Switch to input method like zh/ko/ja + // 3. Click Backspace and Enter on the soft keyboard or physical keyboard + // 4. The physicalKey.usbHidUsage is not correct. + // PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042") + // LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter") + // + // The correct PhysicalKeyboardKey should be + // PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter") + // https://github.com/flutter/flutter/issues/157771 + // We cannot use the debugName to determine the key is correct or not, because it's null in release mode. + // The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f] + // https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600 + final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0; + isMobileAndMapMode = isNormalHsbHidUsage && + // No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)` + // But we still add it for more reliability. + !['Backspace', 'Enter'].contains(e.logicalKey.keyLabel); + } + } + } + final isDesktopAndMapMode = + isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode); + if (isMobileAndMapMode || isDesktopAndMapMode) { + // FIXME: e.character is wrong for dead keys, eg: ^ in de + newKeyboardMode( + e.character ?? '', + e.physicalKey.usbHidUsage & 0xFFFF, + // Show repeat event be converted to "release+press" events? + e is KeyDownEvent || e is KeyRepeatEvent); } else { legacyKeyboardMode(e); } @@ -389,7 +591,33 @@ class InputModel { return KeyEventResult.handled; } - void mapKeyboardMode(RawKeyEvent e) { + /// Send Key Event + void newKeyboardMode(String character, int usbHid, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } + bind.sessionHandleFlutterKeyEvent( + sessionId: sessionId, + character: character, + usbHid: usbHid, + lockModes: lockModes, + downOrUp: down); + } + + void mapKeyboardModeRaw(RawKeyEvent e) { int positionCode = -1; int platformCode = -1; bool down; @@ -441,7 +669,7 @@ class InputModel { .contains(KeyboardLockMode.scrollLock)) { lockModes |= (1 << scrolllock); } - bind.sessionHandleFlutterKeyEvent( + bind.sessionHandleFlutterRawKeyEvent( sessionId: sessionId, name: name, platformCode: platformCode, @@ -450,7 +678,7 @@ class InputModel { downOrUp: down); } - void legacyKeyboardMode(RawKeyEvent e) { + void legacyKeyboardModeRaw(RawKeyEvent e) { if (e is RawKeyDownEvent) { if (e.repeat) { sendRawKey(e, press: true); @@ -471,6 +699,24 @@ class InputModel { inputKey(label, down: down, press: press ?? false); } + void legacyKeyboardMode(KeyEvent e) { + if (e is KeyDownEvent) { + sendKey(e, down: true); + } else if (e is KeyRepeatEvent) { + sendKey(e, press: true); + } else if (e is KeyUpEvent) { + sendKey(e); + } + } + + void sendKey(KeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = physicalKeyMap[e.physicalKey.usbHidUsage] ?? + logicalKeyMap[e.logicalKey.keyId] ?? + e.logicalKey.keyLabel; + inputKey(label, down: down, press: press ?? false); + } + /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). @@ -522,22 +768,22 @@ class InputModel { } /// Send a mouse tap event(down and up). - void tap(MouseButtons button) { - sendMouse('down', button); - sendMouse('up', button); + Future tap(MouseButtons button) async { + await sendMouse('down', button); + await sendMouse('up', button); } - void tapDown(MouseButtons button) { - sendMouse('down', button); + Future tapDown(MouseButtons button) async { + await sendMouse('down', button); } - void tapUp(MouseButtons button) { - sendMouse('up', button); + Future tapUp(MouseButtons button) async { + await sendMouse('up', button); } /// Send scroll event with scroll distance [y]. - void scroll(int y) { - bind.sessionSendMouse( + Future scroll(int y) async { + await bind.sessionSendMouse( sessionId: sessionId, msg: json .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); @@ -558,15 +804,16 @@ class InputModel { } /// Send mouse press event. - void sendMouse(String type, MouseButtons button) { + Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; - bind.sessionSendMouse( + await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'type': type, 'buttons': button.value}))); } void enterOrLeave(bool enter) { - toReleaseKeys.release(handleRawKeyEvent); + toReleaseKeys.release(handleKeyEvent); + toReleaseRawKeys.release(handleRawKeyEvent); _pointerMovedAfterEnter = false; // Fix status @@ -577,14 +824,17 @@ class InputModel { if (!isInputSourceFlutter) { bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); } + if (!isWeb && enter) { + bind.setCurSessionId(sessionId: sessionId); + } } /// Send mouse movement event with distance in [x] and [y]. - void moveMouse(double x, double y) { + Future moveMouse(double x, double y) async { if (!keyboardPerm) return; var x2 = x.toInt(); var y2 = y.toInt(); - bind.sessionSendMouse( + await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } @@ -606,7 +856,7 @@ class InputModel { _stopFling = true; if (isViewOnly) return; if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent('touch', 'pan_start', e.position); + handlePointerEvent('touch', kMouseEventTypePanStart, e.position); } } @@ -649,8 +899,8 @@ class InputModel { } if (x != 0 || y != 0) { if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent( - 'touch', 'pan_update', Offset(x.toDouble(), y.toDouble())); + handlePointerEvent('touch', kMouseEventTypePanUpdate, + Offset(x.toDouble(), y.toDouble())); } else { bind.sessionSendMouse( sessionId: sessionId, @@ -712,7 +962,7 @@ class InputModel { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent('touch', 'pan_end', e.position); + handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); return; } @@ -830,7 +1080,7 @@ class InputModel { onExit: true, ); - int trySetNearestRange(int v, int min, int max, int n) { + static double tryGetNearestRange(double v, double min, double max, double n) { if (v < min && v >= min - n) { v = min; } @@ -870,13 +1120,13 @@ class InputModel { // to-do: handle mouse events late final dynamic evtValue; - if (type == 'pan_update') { + if (type == kMouseEventTypePanUpdate) { evtValue = { 'x': x.toInt(), 'y': y.toInt(), }; } else { - final isMoveTypes = ['pan_start', 'pan_end']; + final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd]; final pos = handlePointerDevicePos( kPointerEventKindTouch, x, @@ -888,8 +1138,8 @@ class InputModel { return; } evtValue = { - 'x': pos.x, - 'y': pos.y, + 'x': pos.x.toInt(), + 'y': pos.y.toInt(), }; } @@ -931,14 +1181,14 @@ class InputModel { return; } - var type = ''; + var type = kMouseEventTypeDefault; var isMove = false; switch (evt['type']) { case _kMouseEventDown: - type = 'down'; + type = kMouseEventTypeDown; break; case _kMouseEventUp: - type = 'up'; + type = kMouseEventTypeUp; break; case _kMouseEventMove: _pointerMovedAfterEnter = true; @@ -949,7 +1199,7 @@ class InputModel { } evt['type'] = type; - if (type == 'down' && !_pointerMovedAfterEnter) { + if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) { // Move mouse to the position of the down event first. lastMousePos = ui.Offset(x, y); refreshMousePos(); @@ -971,8 +1221,8 @@ class InputModel { evt['x'] = '0'; evt['y'] = '0'; } else { - evt['x'] = '${pos.x}'; - evt['y'] = '${pos.y}'; + evt['x'] = '${pos.x.toInt()}'; + evt['y'] = '${pos.y.toInt()}'; } Map mapButtons = { @@ -1112,33 +1362,56 @@ class InputModel { y = pos.dy; } - var evtX = 0; - var evtY = 0; - try { - evtX = x.round(); - evtY = y.round(); - } catch (e) { - debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e'); - return null; - } + return InputModel.getPointInRemoteRect( + true, peerPlatform, kind, evtType, x, y, rect, + buttons: buttons); + } - int minX = rect.left.toInt(); + static Point? getPointInRemoteRect( + bool isLocalDesktop, + String? peerPlatform, + String kind, + String evtType, + double evtX, + double evtY, + Rect rect, + {int buttons = kPrimaryMouseButton}) { + double minX = rect.left; // https://github.com/rustdesk/rustdesk/issues/6678 // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. - int maxX = (rect.left + rect.width).toInt() - + double maxX = (rect.left + rect.width) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); - int minY = rect.top.toInt(); - int maxY = (rect.top + rect.height).toInt() - + double minY = rect.top; + double maxY = (rect.top + rect.height) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); - evtX = trySetNearestRange(evtX, minX, maxX, 5); - evtY = trySetNearestRange(evtY, minY, maxY, 5); - if (kind == kPointerEventKindMouse) { - if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { - // If left mouse up, no early return. - if (!(buttons == kPrimaryMouseButton && evtType == 'up')) { - return null; + evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5); + evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5); + if (isLocalDesktop) { + if (kind == kPointerEventKindMouse) { + if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { + // If left mouse up, no early return. + if (!(buttons == kPrimaryMouseButton && + evtType == kMouseEventTypeUp)) { + return null; + } } } + } else { + bool evtXInRange = evtX >= minX && evtX <= maxX; + bool evtYInRange = evtY >= minY && evtY <= maxY; + if (!(evtXInRange || evtYInRange)) { + return null; + } + if (evtX < minX) { + evtX = minX; + } else if (evtX > maxX) { + evtX = maxX; + } + if (evtY < minY) { + evtY = minY; + } else if (evtY > maxY) { + evtY = maxY; + } } return Point(evtX, evtY); @@ -1164,15 +1437,15 @@ class InputModel { // Simulate a key press event. // `usbHidUsage` is the USB HID usage code of the key. Future tapHidKey(int usbHidUsage) async { - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, true); await Future.delayed(Duration(milliseconds: 100)); - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, false); } Future onMobileVolumeUp() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF); Future onMobileVolumeDown() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF); Future onMobilePower() async => - await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2ad9fdb9b785..748fa1c4043d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,15 +4,20 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; 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'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart'; 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/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; @@ -139,6 +144,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; + bool get isPeerMobile => isPeerAndroid; bool get viewOnly => _viewOnly; @@ -267,6 +273,8 @@ class FfiModel with ChangeNotifier { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, sessionId, peerId); + } else if (name == 'toast') { + handleToast(evt, sessionId, peerId); } else if (name == 'set_multiple_windows_session') { handleMultipleWindowsSession(evt, sessionId, peerId); } else if (name == 'peer_info') { @@ -301,11 +309,18 @@ class FfiModel with ChangeNotifier { .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); + } else if (name == 'empty_dirs') { + parent.target?.fileModel.receiveEmptyDirs(evt); } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - parent.target?.fileModel.jobController.jobDone(evt); - parent.target?.fileModel.refreshAll(); + bool? refresh = + await parent.target?.fileModel.jobController.jobDone(evt); + if (refresh == true) { + // many job done for delete directory + // todo: refresh may not work when confirm delete local directory + parent.target?.fileModel.refreshAll(); + } } else if (name == 'job_error') { parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') { @@ -365,7 +380,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'plugin_option') { handleOption(evt); } else if (name == "sync_peer_hash_password_to_personal_ab") { - if (desktopType == DesktopType.main) { + if (desktopType == DesktopType.main || isWeb || isMobile) { final id = evt['id']; final hash = evt['hash']; if (id != null && hash != null) { @@ -383,6 +398,18 @@ class FfiModel with ChangeNotifier { handleFollowCurrentDisplay(evt, sessionId, peerId); } else if (name == 'use_texture_render') { _handleUseTextureRender(evt, sessionId, peerId); + } else if (name == "selected_files") { + if (isWeb) { + parent.target?.fileModel.onSelectedFiles(evt); + } + } else if (name == "send_emptry_dirs") { + if (isWeb) { + parent.target?.fileModel.sendEmptyDirs(evt); + } + } else if (name == "record_status") { + if (desktopType == DesktopType.remote || isMobile) { + parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); + } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -492,10 +519,12 @@ class FfiModel with ChangeNotifier { newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; - newDisplay.originalWidth = - int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; - newDisplay.originalHeight = - int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; + newDisplay.originalWidth = int.tryParse( + evt['original_width'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; + newDisplay.originalHeight = int.tryParse( + evt['original_height'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; newDisplay._scale = _pi.scaleOfDisplay(display); _pi.displays[display] = newDisplay; @@ -511,7 +540,6 @@ class FfiModel with ChangeNotifier { } } - parent.target?.recordingModel.onSwitchDisplay(); if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) { handleResolutions(peerId, evt['resolutions']); } @@ -582,13 +610,44 @@ class FfiModel with ChangeNotifier { } } + handleToast(Map evt, SessionID sessionId, String peerId) { + final type = evt['type'] ?? 'info'; + final text = evt['text'] ?? ''; + final durMsc = evt['dur_msec'] ?? 2000; + final duration = Duration(milliseconds: durMsc); + if ((text).isEmpty) { + BotToast.showLoading( + duration: duration, + clickClose: true, + allowClick: true, + ); + } else { + if (type.contains('error')) { + BotToast.showText( + contentColor: Colors.red, + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } else { + BotToast.showText( + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } + } + } + /// Show a message box with [type], [title] and [text]. showMsgBox(SessionID sessionId, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { msgBox(sessionId, type, title, text, link, dialogManager, hasCancel: hasCancel, - reconnect: reconnect, + reconnect: hasRetry ? reconnect : null, reconnectTimeout: hasRetry ? _reconnects : null); _timer?.cancel(); if (hasRetry) { @@ -723,6 +782,8 @@ class FfiModel with ChangeNotifier { /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId, bool isCache) async { + parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; + // This call is to ensuer the keyboard mode is updated depending on the peer version. parent.target?.inputModel.updateKeyboardMode(); @@ -786,7 +847,7 @@ class FfiModel with ChangeNotifier { isRefreshing = false; } Map features = json.decode(evt['features']); - _pi.features.privacyMode = features['privacy_mode'] == 1; + _pi.features.privacyMode = features['privacy_mode'] == true; if (!isCache) { handleResolutions(peerId, evt["resolutions"]); } @@ -830,7 +891,7 @@ class FfiModel with ChangeNotifier { for (final mode in [kKeyMapMode, kKeyLegacyMode]) { if (bind.sessionIsKeyboardModeSupported( sessionId: sessionId, mode: mode)) { - bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); + await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); break; } } @@ -1001,14 +1062,15 @@ class FfiModel with ChangeNotifier { // Notify to switch display msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', 'display_is_plugged_out_msg', '', parent.target!.dialogManager); - final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex - ? 0 - : pi.primaryDisplay; - final displays = newDisplay; + final isPeerPrimaryDisplayValid = + pi.primaryDisplay == kInvalidDisplayIndex || + pi.primaryDisplay >= pi.displays.length; + final newDisplay = + isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay; bind.sessionSwitchDisplay( isDesktop: isDesktop, sessionId: sessionId, - value: Int32List.fromList([displays]), + value: Int32List.fromList([newDisplay]), ); if (_pi.isSupportMultiUiSession) { @@ -1085,8 +1147,6 @@ class FfiModel with ChangeNotifier { // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId, {bool updateCursorPos = false}) { - // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. - parent.target?.recordingModel.onClose(); // no need to wait for the response pi.currentDisplay = display; updateCurDisplay(sessionId, updateCursorPos: updateCursorPos); @@ -1175,26 +1235,49 @@ class ImageModel with ChangeNotifier { clearImage() => _image = null; - onRgba(int display, Uint8List rgba) { + bool _webDecodingRgba = false; + final List _webRgbaList = List.empty(growable: true); + webOnRgba(int display, Uint8List rgba) async { + // deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer" + _webRgbaList.add(Uint8List.fromList(rgba)); + if (_webDecodingRgba) { + return; + } + _webDecodingRgba = true; + try { + while (_webRgbaList.isNotEmpty) { + final rgba2 = _webRgbaList.last; + _webRgbaList.clear(); + await decodeAndUpdate(display, rgba2); + } + } catch (e) { + debugPrint('onRgba error: $e'); + } + _webDecodingRgba = false; + } + + onRgba(int display, Uint8List rgba) async { + try { + await decodeAndUpdate(display, rgba); + } catch (e) { + debugPrint('onRgba error: $e'); + } + platformFFI.nextRgba(sessionId, display); + } + + decodeAndUpdate(int display, Uint8List rgba) async { final pid = parent.target?.id; final rect = parent.target?.ffiModel.pi.getDisplayRect(display); - img.decodeImageFromPixels( - rgba, - rect?.width.toInt() ?? 0, - rect?.height.toInt() ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, - onPixelsCopied: () { - // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(sessionId, display); - }).then((image) { - if (parent.target?.id != pid) return; - try { - // my throw exception, because the listener maybe already dispose - update(image); - } catch (e) { - debugPrint('update image: $e'); - } - }); + final image = await img.decodeImageFromPixels( + rgba, + rect?.width.toInt() ?? 0, + rect?.height.toInt() ?? 0, + isWeb | isWindows | isLinux + ? ui.PixelFormat.rgba8888 + : ui.PixelFormat.bgra8888, + ); + if (parent.target?.id != pid) return; + await update(image); } update(ui.Image? image) async { @@ -1202,13 +1285,6 @@ class ImageModel with ChangeNotifier { if (isDesktop || isWebDesktop) { await parent.target?.canvasModel.updateViewStyle(); await parent.target?.canvasModel.updateScrollStyle(); - } else { - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height; - final xscale = canvasWidth / image.width; - final yscale = canvasHeight / image.height; - parent.target?.canvasModel.scale = min(xscale, yscale); } if (parent.target != null) { await initializeCursorAndCanvas(parent.target!); @@ -1220,20 +1296,18 @@ class ImageModel with ChangeNotifier { } // mobile only - // for desktop, height should minus tabbar height double get maxScale { if (_image == null) return 1.5; - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target!.canvasModel.getSize(); final xscale = size.width / _image!.width; final yscale = size.height / _image!.height; return max(1.5, max(xscale, yscale)); } // mobile only - // for desktop, height should minus tabbar height double get minScale { if (_image == null) return 1.5; - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target!.canvasModel.getSize(); final xscale = size.width / _image!.width; final yscale = size.height / _image!.height; return min(xscale, yscale) / 1.5; @@ -1356,6 +1430,12 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); + Timer? _timerMobileFocusCanvasCursor; + + // `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method + // after showing the soft keyboard. + bool isMobileCanvasChanged = false; + final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -1398,21 +1478,37 @@ class CanvasModel with ChangeNotifier { static double get bottomToEdge => isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0; - updateViewStyle({refreshMousePos = true}) async { - Size getSize() { - final size = MediaQueryData.fromWindow(ui.window).size; - // If minimized, w or h may be negative here. - double w = size.width - leftToEdge - rightToEdge; - double h = size.height - topToEdge - bottomToEdge; - return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + Size getSize() { + final mediaData = MediaQueryData.fromView(ui.window); + final size = mediaData.size; + // If minimized, w or h may be negative here. + double w = size.width - leftToEdge - rightToEdge; + double h = size.height - topToEdge - bottomToEdge; + if (isMobile) { + h = h - + mediaData.viewInsets.bottom - + (parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? + 0); } + return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + } + + // mobile only + double getAdjustY() { + final bottom = + parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0; + 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( @@ -1435,16 +1531,25 @@ class CanvasModel with ChangeNotifier { if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { _scale = 1.0 / _devicePixelRatio; } - _x = (size.width - displayWidth * _scale) / 2; - _y = (size.height - displayHeight * _scale) / 2; + _resetCanvasOffset(displayWidth, displayHeight); _imageOverflow.value = _x < 0 || y < 0; - notifyListeners(); - if (refreshMousePos) { + if (notify) { + notifyListeners(); + } + if (!isMobile && refreshMousePos) { parent.target?.inputModel.refreshMousePos(); } tryUpdateScrollStyle(Duration.zero, style); } + _resetCanvasOffset(int displayWidth, int displayHeight) { + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + if (isMobile) { + _moveToCenterCursor(); + } + } + tryUpdateScrollStyle(Duration duration, String? style) async { if (_scrollStyle != ScrollStyle.scrollbar) return; style ??= await bind.sessionGetViewStyle(sessionId: sessionId); @@ -1548,6 +1653,9 @@ class CanvasModel with ChangeNotifier { panX(double dx) { _x += dx; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } @@ -1555,17 +1663,20 @@ class CanvasModel with ChangeNotifier { if (isWebDesktop) { updateViewStyle(); } else { - _x = (size.width - getDisplayWidth() * _scale) / 2; - _y = (size.height - getDisplayHeight() * _scale) / 2; + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); } notifyListeners(); } panY(double dy) { _y += dy; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } + // mobile only updateScale(double v, Offset focalPoint) { if (parent.target?.imageModel.image == null) return; final s = _scale; @@ -1577,21 +1688,34 @@ class CanvasModel with ChangeNotifier { // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; - final adjustForKeyboard = - parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; - // (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY - // _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2 - _y = focalPoint.dy + - adjustForKeyboard - - (focalPoint.dy - _y + adjustForKeyboard) / s * _scale; + final adjust = getAdjustY(); + // (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY + // _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2 + _y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } - clear([bool notify = false]) { + // For reset canvas to the last view style + reset() { + _scale = _lastViewStyle.scale; + _devicePixelRatio = ui.window.devicePixelRatio; + if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) { + _scale = 1.0 / _devicePixelRatio; + } + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); + bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style); + notifyListeners(); + } + + clear() { _x = 0; _y = 0; _scale = 1.0; - if (notify) notifyListeners(); + _lastViewStyle = ViewStyle.defaultViewStyle(); + _timerMobileFocusCanvasCursor?.cancel(); } updateScrollPercent() { @@ -1609,6 +1733,43 @@ class CanvasModel with ChangeNotifier { : 0.0; setScrollPercent(percentX, percentY); } + + void mobileFocusCanvasCursor() { + _timerMobileFocusCanvasCursor?.cancel(); + _timerMobileFocusCanvasCursor = + Timer(Duration(milliseconds: 100), () async { + updateSize(); + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); + notifyListeners(); + }); + } + + // mobile only + // Move the canvas to make the cursor visible(center) on the screen. + void _moveToCenterCursor() { + Rect? imageRect = parent.target?.ffiModel.rect; + if (imageRect == null) { + // unreachable + return; + } + final maxX = 0.0; + final minX = _size.width + (imageRect.left - imageRect.right) * _scale; + final maxY = 0.0; + final minY = _size.height + (imageRect.top - imageRect.bottom) * _scale; + Offset offsetToCenter = + parent.target?.cursorModel.getCanvasOffsetToCenterCursor() ?? + Offset.zero; + if (minX < 0) { + _x = min(max(offsetToCenter.dx, minX), maxX); + } else { + // _size.width > (imageRect.right, imageRect.left) * _scale, we should not change _x + } + if (minY < 0) { + _y = min(max(offsetToCenter.dy, minY), maxY); + } else { + // _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y + } + } } // data for cursor @@ -1802,9 +1963,13 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap() // Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position. bool _lastIsBlocked = false; - double _yForKeyboardAdjust = 0; + bool _lastKeyboardIsVisible = false; - keyHelpToolsVisibilityChanged(Rect? r) { + bool get lastKeyboardIsVisible => _lastKeyboardIsVisible; + + Rect? get keyHelpToolsRectToAdjustCanvas => + _lastKeyboardIsVisible ? _keyHelpToolsRect : null; + keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) { _keyHelpToolsRect = r; if (r == null) { _lastIsBlocked = false; @@ -1814,7 +1979,11 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` will be set when the cursor is moving or touch somewhere else. _lastIsBlocked = true; } - _yForKeyboardAdjust = _y; + if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) { + parent.target?.canvasModel.mobileFocusCanvasCursor(); + parent.target?.canvasModel.isMobileCanvasChanged = false; + } + _lastKeyboardIsVisible = keyboardIsVisible; } get lastIsBlocked => _lastIsBlocked; @@ -1853,8 +2022,10 @@ class CursorModel with ChangeNotifier { addKey(String key) => _cacheKeys.add(key); // remote physical display coordinate + // For update pan (mobile), onOneFingerPanStart, onOneFingerPanUpdate, onHoldDragUpdate Rect getVisibleRect() { - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target?.canvasModel.getSize() ?? + MediaQueryData.fromView(ui.window).size; final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; final scale = parent.target?.canvasModel.scale ?? 1; @@ -1863,22 +2034,22 @@ class CursorModel with ChangeNotifier { return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); } - get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom; - get scale => parent.target?.canvasModel.scale ?? 1.0; - - double adjustForKeyboard() { - if (keyboardHeight < 100) { - return 0.0; - } - - final m = MediaQueryData.fromWindow(ui.window); - final size = m.size; - final thresh = (size.height - keyboardHeight) / 2; - final h = (_yForKeyboardAdjust - getVisibleRect().top) * - scale; // local physical display height - return h - thresh; + Offset getCanvasOffsetToCenterCursor() { + // Cursor should be at the center of the visible rect. + // _x = rect.left + rect.width / 2 + // _y = rect.right + rect.height / 2 + // See `getVisibleRect()` + // _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5; + // _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5; + final size = parent.target?.canvasModel.getSize() ?? + MediaQueryData.fromView(ui.window).size; + final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5; + final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5; + return Offset(xoffset, yoffset); } + get scale => parent.target?.canvasModel.scale ?? 1.0; + // mobile Soft keyboard, block touch event from the KeyHelpTools shouldBlock(double x, double y) { if (!(parent.target?.ffiModel.touchMode ?? false)) { @@ -1893,22 +2064,63 @@ class CursorModel with ChangeNotifier { return false; } - move(double x, double y) { + // For touch mode + Future move(double x, double y) async { if (shouldBlock(x, y)) { _lastIsBlocked = true; return false; } _lastIsBlocked = false; - moveLocal(x, y, adjust: adjustForKeyboard()); - parent.target?.inputModel.moveMouse(_x, _y); + if (!_moveLocalIfInRemoteRect(x, y)) { + return false; + } + await parent.target?.inputModel.moveMouse(_x, _y); return true; } - moveLocal(double x, double y, {double adjust = 0}) { + bool isInRemoteRect(Offset offset) { + return getRemotePosInRect(offset) != null; + } + + Offset? getRemotePosInRect(Offset offset) { + final adjust = parent.target?.canvasModel.getAdjustY() ?? 0; + final newPos = _getNewPos(offset.dx, offset.dy, adjust); + final visibleRect = getVisibleRect(); + if (!isPointInRect(newPos, visibleRect)) { + return null; + } + final rect = parent.target?.ffiModel.rect; + if (rect != null) { + if (!isPointInRect(newPos, rect)) { + return null; + } + } + return newPos; + } + + Offset _getNewPos(double x, double y, double adjust) { final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; - _x = (x - xoffset) / scale + _displayOriginX; - _y = (y - yoffset + adjust) / scale + _displayOriginY; + final newX = (x - xoffset) / scale + _displayOriginX; + final newY = (y - yoffset - adjust) / scale + _displayOriginY; + return Offset(newX, newY); + } + + bool _moveLocalIfInRemoteRect(double x, double y) { + final newPos = getRemotePosInRect(Offset(x, y)); + if (newPos == null) { + return false; + } + _x = newPos.dx; + _y = newPos.dy; + notifyListeners(); + return true; + } + + moveLocal(double x, double y, {double adjust = 0}) { + final newPos = _getNewPos(x, y, adjust); + _x = newPos.dx; + _y = newPos.dy; notifyListeners(); } @@ -1916,13 +2128,13 @@ class CursorModel with ChangeNotifier { _x = _displayOriginX; _y = _displayOriginY; parent.target?.inputModel.moveMouse(_x, _y); - parent.target?.canvasModel.clear(true); + parent.target?.canvasModel.reset(); notifyListeners(); } - updatePan(Offset delta, Offset localPosition, bool touchMode) { + updatePan(Offset delta, Offset localPosition, bool touchMode) async { if (touchMode) { - _handleTouchMode(delta, localPosition); + await _handleTouchMode(delta, localPosition); return; } double dx = delta.dx; @@ -1980,13 +2192,34 @@ class CursorModel with ChangeNotifier { } if (dx == 0 && dy == 0) return; - _x += dx; - _y += dy; + + Point? newPos; + final rect = parent.target?.ffiModel.rect; + if (rect == null) { + // unreachable + return; + } + newPos = InputModel.getPointInRemoteRect( + false, + parent.target?.ffiModel.pi.platform, + kPointerEventKindMouse, + kMouseEventTypeDefault, + _x + dx, + _y + dy, + rect, + buttons: kPrimaryButton); + if (newPos == null) { + return; + } + dx = newPos.x - _x; + dy = newPos.y - _y; + _x = newPos.x; + _y = newPos.y; if (tryMoveCanvasX && dx != 0) { - parent.target?.canvasModel.panX(-dx); + parent.target?.canvasModel.panX(-dx * scale); } if (tryMoveCanvasY && dy != 0) { - parent.target?.canvasModel.panY(-dy); + parent.target?.canvasModel.panY(-dy * scale); } parent.target?.inputModel.moveMouse(_x, _y); @@ -1999,7 +2232,7 @@ class CursorModel with ChangeNotifier { return x >= 0 && y >= 0 && x <= w && y <= h; } - _handleTouchMode(Offset delta, Offset localPosition) { + _handleTouchMode(Offset delta, Offset localPosition) async { bool isMoved = false; if (_remoteWindowCoords.isNotEmpty && _windowRect != null && @@ -2015,15 +2248,50 @@ class CursorModel with ChangeNotifier { coords.canvas.scale; x2 += coords.cursor.offset.dx; y2 += coords.cursor.offset.dy; - parent.target?.inputModel.moveMouse(x2, y2); + await parent.target?.inputModel.moveMouse(x2, y2); isMoved = true; } } if (!isMoved) { + final rect = parent.target?.ffiModel.rect; + if (rect == null) { + // unreachable + return; + } + + Offset? movementInRect(double x, double y, Rect r) { + final isXInRect = x >= r.left && x <= r.right; + final isYInRect = y >= r.top && y <= r.bottom; + if (!(isXInRect || isYInRect)) { + return null; + } + if (x < r.left) { + x = r.left; + } else if (x > r.right) { + x = r.right; + } + if (y < r.top) { + y = r.top; + } else if (y > r.bottom) { + y = r.bottom; + } + return Offset(x, y); + } + final scale = parent.target?.canvasModel.scale ?? 1.0; - _x += delta.dx / scale; - _y += delta.dy / scale; - parent.target?.inputModel.moveMouse(_x, _y); + var movement = + movementInRect(_x + delta.dx / scale, _y + delta.dy / scale, rect); + if (movement == null) { + return; + } + movement = movementInRect(movement.dx, movement.dy, getVisibleRect()); + if (movement == null) { + return; + } + + _x = movement.dx; + _y = movement.dy; + await parent.target?.inputModel.moveMouse(_x, _y); } notifyListeners(); } @@ -2175,6 +2443,7 @@ class CursorModel with ChangeNotifier { debugPrint("deleting cursor with key $k"); deleteCustomCursor(k); } + resetSystemCursor(); } trySetRemoteWindowCoords() { @@ -2221,8 +2490,10 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { - if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; - if ((evt['fps'] as String).isNotEmpty) { + if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) { + _data.speed = evt['speed']; + } + if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) { final fps = jsonDecode(evt['fps']) as Map; final pi = parent.target?.ffiModel.pi; if (pi != null) { @@ -2243,14 +2514,18 @@ class QualityMonitorModel with ChangeNotifier { _data.fps = null; } } - if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; - if ((evt['target_bitrate'] as String).isNotEmpty) { + if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) { + _data.delay = evt['delay']; + } + if (evt.containsKey('target_bitrate') && + (evt['target_bitrate'] as String).isNotEmpty) { _data.targetBitrate = evt['target_bitrate']; } - if ((evt['codec_format'] as String).isNotEmpty) { + if (evt.containsKey('codec_format') && + (evt['codec_format'] as String).isNotEmpty) { _data.codecFormat = evt['codec_format']; } - if ((evt['chroma'] as String).isNotEmpty) { + if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) { _data.chroma = evt['chroma']; } notifyListeners(); @@ -2264,25 +2539,7 @@ class RecordingModel with ChangeNotifier { WeakReference parent; RecordingModel(this.parent); bool _start = false; - get start => _start; - - onSwitchDisplay() { - if (isIOS || !_start) return; - final sessionId = parent.target?.sessionId; - int? width = parent.target?.canvasModel.getDisplayWidth(); - int? height = parent.target?.canvasModel.getDisplayHeight(); - if (sessionId == null || width == null || height == null) return; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - bind.sessionRecordScreen( - sessionId: sessionId, - start: true, - display: currentDisplay, - width: width, - height: height); - } + bool get start => _start; toggle() async { if (isIOS) return; @@ -2290,48 +2547,16 @@ class RecordingModel with ChangeNotifier { if (sessionId == null) return; final pi = parent.target?.ffiModel.pi; if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - _start = !_start; - notifyListeners(); - await _sendStatusMessage(sessionId, pi, _start); - if (_start) { - sessionRefreshVideo(sessionId, pi); - if (versionCmp(pi.version, '1.2.4') >= 0) { - // will not receive SwitchDisplay since 1.2.4 - onSwitchDisplay(); - } - } else { - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); + bool value = !_start; + if (value) { + await sessionRefreshVideo(sessionId, pi); } + await bind.sessionRecordScreen(sessionId: sessionId, start: value); } - onClose() async { - if (isIOS) return; - final sessionId = parent.target?.sessionId; - if (sessionId == null) return; - if (!_start) return; - _start = false; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - await _sendStatusMessage(sessionId, pi, false); - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); - } - - _sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async { - await bind.sessionRecordStatus(sessionId: sessionId, status: status); + updateStatus(bool status) { + _start = status; + notifyListeners(); } } @@ -2380,6 +2605,9 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session + late final Peers recentPeersModel; // global + late final Peers favoritePeersModel; // global + late final Peers lanPeersModel; // global FFI(SessionID? sId) { sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); @@ -2400,6 +2628,16 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); + recentPeersModel = Peers( + name: PeersModelName.recent, + loadEvent: LoadEvent.recent, + getInitPeers: null); + favoritePeersModel = Peers( + name: PeersModelName.favorite, + loadEvent: LoadEvent.favorite, + getInitPeers: null); + lanPeersModel = Peers( + name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null); } /// Mobile reuse FFI @@ -2420,6 +2658,7 @@ class FFI { String? switchUuid, String? password, bool? isSharedPassword, + String? connToken, bool? forceRelay, int? tabWindowId, int? display, @@ -2456,6 +2695,7 @@ class FFI { forceRelay: forceRelay ?? false, password: password ?? '', isSharedPassword: isSharedPassword ?? false, + connToken: connToken, ); } else if (display != null) { if (displays == null) { @@ -2494,6 +2734,7 @@ class FFI { onEvent2UIRgba(); imageModel.onRgba(display, data); }); + this.id = id; return; } @@ -2558,7 +2799,7 @@ class FFI { final rgba = platformFFI.getRgba(sessionId, display, sz); if (rgba != null) { onEvent2UIRgba(); - imageModel.onRgba(display, rgba); + await imageModel.onRgba(display, rgba); } else { platformFFI.nextRgba(sessionId, display); } @@ -2608,8 +2849,9 @@ class FFI { remember: remember); } - void send2FA(SessionID sessionId, String code) { - bind.sessionSend2Fa(sessionId: sessionId, code: code); + void send2FA(SessionID sessionId, String code, bool trustThisDevice) { + bind.sessionSend2Fa( + sessionId: sessionId, code: code, trustThisDevice: trustThisDevice); } /// Close the remote session. @@ -2626,7 +2868,8 @@ class FFI { canvasModel.scale, ffiModel.pi.currentDisplay); } - imageModel.update(null); + imageModel.callbacksOnFirstImage.clear(); + await imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index b99cf2e7fb82..c8d5085e8973 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -48,6 +48,12 @@ class PlatformFFI { static get isMain => instance._appType == kAppTypeMain; + static String getByName(String name, [String arg = '']) { + return ''; + } + + static void setByName(String name, [String value = '']) {} + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -276,4 +282,6 @@ class PlatformFFI { void syncAndroidServiceAppDirConfigPath() { invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); } + + void setFullscreenCallback(void Function(bool) fun) {} } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 188dd4e0bdfd..7ab5a2b803ee 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -194,10 +194,14 @@ class Peers extends ChangeNotifier { } void _updateOnlineState(Map evt) { + int changedCount = 0; evt['onlines'].split(',').forEach((online) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == online) { - peers[i].online = true; + if (!peers[i].online) { + changedCount += 1; + peers[i].online = true; + } } } }); @@ -205,13 +209,18 @@ class Peers extends ChangeNotifier { evt['offlines'].split(',').forEach((offline) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == offline) { - peers[i].online = false; + if (peers[i].online) { + changedCount += 1; + peers[i].online = false; + } } } }); - event = UpdateEvent.online; - notifyListeners(); + if (changedCount > 0) { + event = UpdateEvent.online; + notifyListeners(); + } } void _updatePeers(Map evt) { diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 3c0fe636d685..83df1f05d6aa 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier { // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 // After onTap, the shift key should be pressed for a while when not in multiselection mode, // because onTap is delayed when onDoubleTap is not null - if (isDesktop && !_isShiftDown) return; + if (isDesktop || isWebDesktop) return; _multiSelectionMode = true; } final cached = _currentTabCachedPeers.map((e) => e.id).toList(); @@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier { notifyListeners(); } + // `notifyListeners()` will cause many rebuilds. + // So, we need to reduce the calls to "notifyListeners()" only when necessary. + // A better way is to use a new model. setCurrentTabCachedPeers(List peers) { Future.delayed(Duration.zero, () { + final isPreEmpty = _currentTabCachedPeers.isEmpty; _currentTabCachedPeers = peers; - notifyListeners(); + final isNowEmpty = _currentTabCachedPeers.isEmpty; + if (isPreEmpty != isNowEmpty) { + notifyListeners(); + } }); } diff --git a/flutter/lib/models/platform_model.dart b/flutter/lib/models/platform_model.dart index 6bc770ff666b..0f21587ad8df 100644 --- a/flutter/lib/models/platform_model.dart +++ b/flutter/lib/models/platform_model.dart @@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance; final localeName = PlatformFFI.localeName; RustdeskImpl get bind => platformFFI.ffiBind; + +String ffiGetByName(String name, [String arg = '']) { + return PlatformFFI.getByName(name, arg); +} + +void ffiSetByName(String name, [String value = '']) { + PlatformFFI.setByName(name, value); +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 613fee6ad117..8775764619ee 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; + bool _clipboardOk = false; bool _showElevation = false; bool hideCm = false; int _connectStatus = 0; // Rendezvous Server status @@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier { bool get fileOk => _fileOk; + bool get clipboardOk => _clipboardOk; + bool get showElevation => _showElevation; int get connectStatus => _connectStatus; @@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption != 'N'; } + // clipboard + final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard); + _clipboardOk = clipOption != 'N'; + notifyListeners(); } @@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + toggleClipboard() async { + _clipboardOk = !clipboardOk; + bind.mainSetOption( + key: kOptionEnableClipboard, + value: clipboardOk ? defaultOptionYes : 'N'); + notifyListeners(); + } + toggleInput() async { if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); @@ -826,7 +841,7 @@ class Client { Map toJson() { final Map data = {}; data['id'] = id; - data['is_start'] = authorized; + data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; data['port_forward'] = portForward; data['name'] = name; @@ -840,6 +855,8 @@ class Client { data['block_input'] = blockInput; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; + data['in_voice_call'] = inVoiceCall; + data['incoming_voice_call'] = incomingVoiceCall; return data; } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 3c514aaaadc5..2e1b516df01a 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,11 +14,19 @@ class StateGlobal { bool _isMinimized = false; final RxBool isMaximized = false.obs; final RxBool _showTabBar = true.obs; - final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize); + final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; + final RxInt videoConnCount = 0.obs; final RxBool isFocused = false.obs; + // for mobile and web + bool isInMainPage = true; + bool isWebVisible = true; + + final isPortrait = false.obs; + + final updateUrl = ''.obs; String _inputSource = ''; @@ -68,32 +76,45 @@ class StateGlobal { if (_fullscreen.value != v) { _fullscreen.value = v; _showTabBar.value = !_fullscreen.value; - refreshResizeEdgeSize(); - print( - "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); - _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; - if (procWnd) { - final wc = WindowController.fromWindowId(windowId); - wc.setFullscreen(_fullscreen.isTrue).then((_) { - // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 - if (isWindows && !v) { - Future.delayed(Duration.zero, () async { - final frame = await wc.getFrame(); - final newRect = Rect.fromLTWH( - frame.left, frame.top, frame.width + 1, frame.height + 1); - await wc.setFrame(newRect); - }); - } - }); + if (isWebDesktop) { + procFullscreenWeb(); + } else { + procFullscreenNative(procWnd); } } } + procFullscreenWeb() { + final isFullscreen = ffiGetByName('fullscreen') == 'Y'; + String fullscreenValue = ''; + if (isFullscreen && _fullscreen.isFalse) { + fullscreenValue = 'N'; + } else if (!isFullscreen && fullscreen.isTrue) { + fullscreenValue = 'Y'; + } + if (fullscreenValue.isNotEmpty) { + ffiSetByName('fullscreen', fullscreenValue); + } + } + + procFullscreenNative(bool procWnd) { + refreshResizeEdgeSize(); + print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); + _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; + if (procWnd) { + final wc = WindowController.fromWindowId(windowId); + wc.setFullscreen(_fullscreen.isTrue).then((_) { + // We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced. + // https://github.com/rustdesk/rustdesk/issues/9675 + }); + } + } + refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue ? kFullScreenEdgeSize : isMaximized.isTrue ? kMaximizeEdgeSize - : windowEdgeSize; + : windowResizeEdgeSize; String getInputSource({bool force = false}) { if (force || _inputSource.isEmpty) { @@ -107,7 +128,13 @@ class StateGlobal { _inputSource = bind.mainGetInputSource(); } - StateGlobal._(); + StateGlobal._() { + if (isWebDesktop) { + platformFFI.setFullscreenCallback((v) { + _fullscreen.value = v; + }); + } + } static final StateGlobal instance = StateGlobal._(); } diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 4e7f881ad0f6..9d9c762d9981 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -17,13 +17,23 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; final RxBool isAdmin = false.obs; + final RxString networkError = ''.obs; bool get isLogin => userName.isNotEmpty; WeakReference parent; - UserModel(this.parent); + UserModel(this.parent) { + userName.listen((p0) { + // When user name becomes empty, show login button + // When user name becomes non-empty: + // For _updateLocalUserInfo, network error will be set later + // For login success, should clear network error + networkError.value = ''; + }); + } void refreshCurrentUser() async { if (bind.isDisableAccount()) return; + networkError.value = ''; final token = bind.mainGetLocalOption(key: 'access_token'); if (token == '') { await updateOtherModels(); @@ -38,12 +48,18 @@ class UserModel { if (refreshingUser) return; try { refreshingUser = true; - final response = await http.post(Uri.parse('$url/api/currentUser'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token' - }, - body: json.encode(body)); + final http.Response response; + try { + response = await http.post(Uri.parse('$url/api/currentUser'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' + }, + body: json.encode(body)); + } catch (e) { + networkError.value = e.toString(); + rethrow; + } refreshingUser = false; final status = response.statusCode; if (status == 401 || status == 400) { diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 4896781a9cc1..a4312d959c72 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -1,12 +1,14 @@ // ignore_for_file: avoid_web_libraries_in_flutter import 'dart:convert'; +import 'dart:js_interop'; import 'dart:typed_data'; import 'dart:js'; import 'dart:html'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/web/bridge.dart'; import 'package:flutter_hbb/common.dart'; @@ -28,7 +30,15 @@ class PlatformFFI { context.callMethod('setByName', [name, value]); } - PlatformFFI._(); + PlatformFFI._() { + window.document.addEventListener( + 'visibilitychange', + (event) => { + stateGlobal.isWebVisible = + window.document.visibilityState == 'visible' + }); + } + static final PlatformFFI instance = PlatformFFI._(); static get localeName => window.navigator.language; @@ -98,6 +108,10 @@ class PlatformFFI { sessionId: sessionId, display: display, ptr: ptr); Future init(String appType) async { + Completer completer = Completer(); + context["onInitFinished"] = () { + completer.complete(); + }; context.callMethod('init'); version = getByName('version'); window.onContextMenu.listen((event) { @@ -112,6 +126,7 @@ class PlatformFFI { print('json.decode fail(): $e'); } }; + return completer.future; } void setEventCallback(void Function(Map) fun) { @@ -157,4 +172,10 @@ class PlatformFFI { // just for compilation void syncAndroidServiceAppDirConfigPath() {} + + void setFullscreenCallback(void Function(bool) fun) { + context["onFullscreenChanged"] = (bool v) { + fun(v); + }; + } } diff --git a/flutter/lib/native/common.dart b/flutter/lib/native/common.dart index d3888a245d10..96d5bd6e82ac 100644 --- a/flutter/lib/native/common.dart +++ b/flutter/lib/native/common.dart @@ -11,3 +11,7 @@ final isWebDesktop_ = false; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; String get screenInfo_ => ''; + +final isWebOnWindows_ = false; +final isWebOnLinux_ = false; +final isWebOnMacOS_ = false; diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart index 3e53f3cc5b25..e85d42a55893 100644 --- a/flutter/lib/native/custom_cursor.dart +++ b/flutter/lib/native/custom_cursor.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart'; deleteCustomCursor(String key) => custom_cursor_manager.CursorManager.instance.deleteCursor(key); +resetSystemCursor() {} MouseCursor buildCursorOfCache( CursorModel cursor, double scale, CursorData? cache) { diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 6280555ba4f4..d5042e61ad2a 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -13,14 +13,12 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, - VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds bool allowUpscaling = true, }) async { if (targetWidth != null) { assert(allowUpscaling || targetWidth <= width); if (!(allowUpscaling || targetWidth <= width)) { print("not allow upscaling but targetWidth > width"); - onPixelsCopied?.call(); return null; } } @@ -28,7 +26,6 @@ Future decodeImageFromPixels( assert(allowUpscaling || targetHeight <= height); if (!(allowUpscaling || targetHeight <= height)) { print("not allow upscaling but targetHeight > height"); - onPixelsCopied?.call(); return null; } } @@ -36,9 +33,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer; try { buffer = await ui.ImmutableBuffer.fromUint8List(pixels); - onPixelsCopied?.call(); } catch (e) { - onPixelsCopied?.call(); return null; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 191152c8625a..70001ffdff4c 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -124,6 +124,9 @@ class RustDeskMultiWindowManager { bool withScreenRect, ) async { final windowController = await DesktopMultiWindow.createWindow(msg); + if (isWindows) { + windowController.setInitBackgroundColor(Colors.black); + } final windowId = windowController.windowId; if (!withScreenRect) { windowController @@ -198,6 +201,7 @@ class RustDeskMultiWindowManager { String? switchUuid, bool? isRDP, bool? isSharedPassword, + String? connToken, }) async { var params = { "type": type.index, @@ -214,6 +218,9 @@ class RustDeskMultiWindowManager { if (isSharedPassword != null) { params['isSharedPassword'] = isSharedPassword; } + if (connToken != null) { + params['connToken'] = connToken; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -251,8 +258,13 @@ class RustDeskMultiWindowManager { ); } - Future newFileTransfer(String remoteId, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newFileTransfer( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.FileTransfer, kWindowEventNewFileTransfer, @@ -261,11 +273,18 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, isSharedPassword: isSharedPassword, + connToken: connToken, ); } - Future newPortForward(String remoteId, bool isRDP, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newPortForward( + String remoteId, + bool isRDP, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.PortForward, kWindowEventNewPortForward, @@ -275,6 +294,7 @@ class RustDeskMultiWindowManager { forceRelay: forceRelay, isRDP: isRDP, isSharedPassword: isSharedPassword, + connToken: connToken, ); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d4939c804e60..dba7fc0941c8 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; +import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; @@ -23,6 +24,7 @@ sealed class EventToUI { ) = EventToUI_Rgba; const factory EventToUI.texture( int field0, + bool field1, ) = EventToUI_Texture; } @@ -33,25 +35,29 @@ class EventToUI_Event implements EventToUI { } class EventToUI_Rgba implements EventToUI { - const EventToUI_Rgba(final int field0) : this.field = field0; + const EventToUI_Rgba(final int field0) : field = field0; final int field; int get field0 => field; } class EventToUI_Texture implements EventToUI { - const EventToUI_Texture(final int field0) : this.field = field0; - final int field; - int get field0 => field; + const EventToUI_Texture(final int field0, final bool field1) + : f0 = field0, + f1 = field1; + final int f0; + final bool f1; + int get field0 => f0; + bool get field1 => f1; } class RustdeskImpl { Future stopGlobalEventStream({required String appType, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("stopGlobalEventStream"); } Future hostStopSystemKeyPropagate( {required bool stopped, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("hostStopSystemKeyPropagate"); } int peerGetDefaultSessionsCount({required String id, dynamic hint}) { @@ -76,10 +82,16 @@ class RustdeskImpl { required bool forceRelay, required String password, required bool isSharedPassword, + String? connToken, dynamic hint}) { return js.context.callMethod('setByName', [ 'session_add_sync', - jsonEncode({'id': id, 'password': password}) + jsonEncode({ + 'id': id, + 'password': password, + 'is_shared_password': isSharedPassword, + 'isFileTransfer': isFileTransfer + }) ]); } @@ -97,7 +109,7 @@ class RustdeskImpl { required String id, required Int32List displays, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionStartWithDisplays"); } Future sessionGetRemember( @@ -142,8 +154,14 @@ class RustdeskImpl { } Future sessionSend2Fa( - {required UuidValue sessionId, required String code, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); + {required UuidValue sessionId, + required String code, + required bool trustThisDevice, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_2fa', + jsonEncode({'code': code, 'trust_this_device': trustThisDevice}) + ])); } Future sessionClose({required UuidValue sessionId, dynamic hint}) { @@ -156,18 +174,12 @@ class RustdeskImpl { } Future sessionRecordScreen( - {required UuidValue sessionId, - required bool start, - required int display, - required int width, - required int height, - dynamic hint}) { - throw UnimplementedError(); + {required UuidValue sessionId, required bool start, dynamic hint}) { + throw UnimplementedError("sessionRecordScreen"); } - Future sessionRecordStatus( - {required UuidValue sessionId, required bool status, dynamic hint}) { - throw UnimplementedError(); + bool sessionGetIsRecording({required UuidValue sessionId, dynamic hint}) { + return false; } Future sessionReconnect( @@ -178,7 +190,7 @@ class RustdeskImpl { Future sessionToggleOption( {required UuidValue sessionId, required String value, dynamic hint}) { return Future( - () => js.context.callMethod('setByName', ['toggle_option', value])); + () => js.context.callMethod('setByName', ['option:toggle', value])); } Future sessionTogglePrivacyMode( @@ -187,8 +199,8 @@ class RustdeskImpl { required bool on, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'toggle_option', - jsonEncode({implKey, on}) + 'toggle_privacy_mode', + jsonEncode({'impl_key': implKey, 'on': on}) ])); } @@ -226,7 +238,7 @@ class RustdeskImpl { } String getLocalKbLayoutType({dynamic hint}) { - throw js.context.callMethod('getByName', ['option:local', 'kb_layout']); + return js.context.callMethod('getByName', ['option:local', 'kb_layout']); } Future setLocalKbLayoutType( @@ -267,16 +279,14 @@ class RustdeskImpl { Future sessionGetImageQuality( {required UuidValue sessionId, dynamic hint}) { - return Future(() => js.context - .callMethod('getByName', ['option:session', 'image_quality'])); + return Future(() => js.context.callMethod('getByName', ['image_quality'])); } Future sessionSetImageQuality( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'image_quality', 'value': value}) - ])); + print('set image quality: $value'); + return Future( + () => js.context.callMethod('setByName', ['image_quality', value])); } Future sessionGetKeyboardMode( @@ -343,7 +353,11 @@ class RustdeskImpl { bool sessionIsKeyboardModeSupported( {required UuidValue sessionId, required String mode, dynamic hint}) { - return mode == kKeyLegacyMode; + if (mainGetInputSource(hint: hint) == 'Input source 1') { + return [kKeyMapMode, kKeyTranslateMode].contains(mode); + } else { + return [kKeyLegacyMode, kKeyMapMode].contains(mode); + } } bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { @@ -353,17 +367,15 @@ class RustdeskImpl { Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_image_quality', 'value': value}) + 'custom_image_quality', + value, ])); } Future sessionSetCustomFps( {required UuidValue sessionId, required int fps, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_fps', 'value': fps}) - ])); + return Future( + () => js.context.callMethod('setByName', ['custom-fps', fps])); } Future sessionLockScreen({required UuidValue sessionId, dynamic hint}) { @@ -382,14 +394,32 @@ class RustdeskImpl { return Future(() => js.context.callMethod('setByName', [ 'switch_display', jsonEncode({ - isDesktop: isDesktop, - sessionId: sessionId.toString(), - value: value + 'isDesktop': isDesktop, + 'sessionId': sessionId.toString(), + 'value': value }) ])); } Future sessionHandleFlutterKeyEvent( + {required UuidValue sessionId, + required String character, + required int usbHid, + required int lockModes, + required bool downOrUp, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'flutter_key_event', + jsonEncode({ + 'name': character, + 'usb_hid': usbHid, + 'lock_modes': lockModes, + if (downOrUp) 'down': 'true', + }) + ])); + } + + Future sessionHandleFlutterRawKeyEvent( {required UuidValue sessionId, required String name, required int platformCode, @@ -397,13 +427,12 @@ class RustdeskImpl { required int lockModes, required bool downOrUp, dynamic hint}) { - // TODO: map mode - throw UnimplementedError(); + throw UnimplementedError("sessionHandleFlutterRawKeyEvent"); } void sessionEnterOrLeave( {required UuidValue sessionId, required bool enter, dynamic hint}) { - throw UnimplementedError(); + js.context.callMethod('setByName', ['enter_or_leave', enter]); } Future sessionInputKey( @@ -438,7 +467,8 @@ class RustdeskImpl { Future sessionSendChat( {required UuidValue sessionId, required String text, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_chat', text])); } Future sessionPeerOption( @@ -446,7 +476,7 @@ class RustdeskImpl { required String name, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('SetByName', [ + return Future(() => js.context.callMethod('setByName', [ 'option:session', jsonEncode({'name': name, 'value': value}) ])); @@ -469,7 +499,10 @@ class RustdeskImpl { required String path, required bool includeHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_remote_dir', + jsonEncode({'path': path, 'include_hidden': includeHidden}) + ])); } Future sessionSendFiles( @@ -480,8 +513,20 @@ class RustdeskImpl { required int fileNum, required bool includeHidden, required bool isRemote, + required bool isDir, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'send_files', + jsonEncode({ + 'id': actId, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + 'is_dir': isDir, + }) + ])); } Future sessionSetConfirmOverrideFile( @@ -492,7 +537,16 @@ class RustdeskImpl { required bool remember, required bool isUpload, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'confirm_override_file', + jsonEncode({ + 'id': actId, + 'file_num': fileNum, + 'need_override': needOverride, + 'remember': remember, + 'is_upload': isUpload + }) + ])); } Future sessionRemoveFile( @@ -502,17 +556,33 @@ class RustdeskImpl { required int fileNum, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'file_num': fileNum, + 'is_remote': isRemote + }) + ])); } - Future sessionReadDirRecursive( + Future sessionReadDirToRemoveRecursive( {required UuidValue sessionId, required int actId, required String path, required bool isRemote, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_dir_to_remove_recursive', + jsonEncode({ + 'id': actId, + 'path': path, + 'is_remote': isRemote, + 'show_hidden': showHidden + }) + ])); } Future sessionRemoveAllEmptyDirs( @@ -521,12 +591,16 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_all_empty_dirs', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionCancelJob( {required UuidValue sessionId, required int actId, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['cancel_job', actId])); } Future sessionCreateDir( @@ -535,7 +609,10 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'create_dir', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionReadLocalDirSync( @@ -543,17 +620,21 @@ class RustdeskImpl { required String path, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionReadLocalDirSync"); } Future sessionGetPlatform( {required UuidValue sessionId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + if (isRemote) { + return Future(() => js.context.callMethod('getByName', ['platform'])); + } else { + return Future(() => 'Web'); + } } Future sessionLoadLastTransferJobs( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionLoadLastTransferJobs"); } Future sessionAddJob( @@ -565,7 +646,7 @@ class RustdeskImpl { required bool includeHidden, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddJob"); } Future sessionResumeJob( @@ -573,12 +654,12 @@ class RustdeskImpl { required int actId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionResumeJob"); } Future sessionElevateDirect( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['elevate_direct'])); } Future sessionElevateWithLogon( @@ -588,13 +669,13 @@ class RustdeskImpl { dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ 'elevate_with_logon', - jsonEncode({username, password}) + jsonEncode({'username': username, 'password': password}) ])); } Future sessionSwitchSides( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSwitchSides"); } Future sessionChangeResolution( @@ -604,7 +685,10 @@ class RustdeskImpl { required int height, dynamic hint}) { // note: restore on disconnected - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'change_resolution', + jsonEncode({'display': display, 'width': width, 'height': height}) + ])); } Future sessionSetSize( @@ -618,27 +702,36 @@ class RustdeskImpl { Future sessionSendSelectedSessionId( {required UuidValue sessionId, required String sid, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['selected_sid', sid])); } Future> mainGetSoundInputs({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSoundInputs"); } Future mainGetDefaultSoundInput({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDefaultSoundInput"); } String mainGetLoginDeviceInfo({dynamic hint}) { - throw UnimplementedError(); + String userAgent = html.window.navigator.userAgent; + String appName = html.window.navigator.appName; + String appVersion = html.window.navigator.appVersion; + String? platform = html.window.navigator.platform; + return jsonEncode({ + 'os': '$userAgent, $appName $appVersion ($platform)', + 'type': 'Web client', + 'name': js.context.callMethod('getByName', ['my_name']), + }); } Future mainChangeId({required String newId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeId"); } Future mainGetAsyncStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetAsyncStatus"); } Future mainGetOption({required String key, dynamic hint}) { @@ -650,11 +743,11 @@ class RustdeskImpl { } Future mainGetError({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetError"); } bool mainShowOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainShowOption"); } Future mainSetOption( @@ -691,27 +784,28 @@ class RustdeskImpl { required String username, required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetSocks"); } Future> mainGetSocks({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSocks"); } Future mainGetAppName({dynamic hint}) { - throw UnimplementedError(); + return Future.value(mainGetAppNameSync(hint: hint)); } String mainGetAppNameSync({dynamic hint}) { - throw UnimplementedError(); + return 'RustDesk'; } String mainUriPrefixSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUriPrefixSync"); } Future mainGetLicense({dynamic hint}) { - throw UnimplementedError(); + // TODO: implement + return Future(() => ''); } Future mainGetVersion({dynamic hint}) { @@ -738,11 +832,11 @@ class RustdeskImpl { String mainGetPeerSync({required String id, dynamic hint}) { // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainGetPeerSync"); } Future mainGetLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetLanPeers"); } Future mainGetConnectStatus({dynamic hint}) { @@ -751,16 +845,17 @@ class RustdeskImpl { } Future mainCheckConnectStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckConnectStatus"); } Future mainIsUsingPublicServer({dynamic hint}) { - return Future( - () => js.context.callMethod('setByName', ["is_using_public_server"])); + return Future(() => + js.context.callMethod('getByName', ["is_using_public_server"]) == + 'true'); } Future mainDiscover({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDiscover"); } Future mainGetApiServer({dynamic hint}) { @@ -772,7 +867,7 @@ class RustdeskImpl { required String body, required String header, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainPostRequest"); } Future mainGetProxyStatus({dynamic hint}) { @@ -786,11 +881,11 @@ class RustdeskImpl { required String header, dynamic hint, }) { - throw UnimplementedError(); + throw UnimplementedError("mainHttpRequest"); } Future mainGetHttpStatus({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHttpStatus"); } String mainGetLocalOption({required String key, dynamic hint}) { @@ -798,7 +893,7 @@ class RustdeskImpl { } String mainGetEnv({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetEnv"); } Future mainSetLocalOption( @@ -810,24 +905,29 @@ class RustdeskImpl { } String mainGetInputSource({dynamic hint}) { - // // rdev grab mode - // const CONFIG_INPUT_SOURCE_1 = "Input source 1"; + final inputSource = + js.context.callMethod('getByName', ['option:local', 'input-source']); + // // js grab mode + // export const CONFIG_INPUT_SOURCE_1 = "Input source 1"; // // flutter grab mode - // const CONFIG_INPUT_SOURCE_2 = "Input source 2"; - return 'Input source 2'; + // export const CONFIG_INPUT_SOURCE_2 = "Input source 2"; + return inputSource != '' ? inputSource : 'Input source 1'; } Future mainSetInputSource( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future.value(); + return Future(() => js.context.callMethod('setByName', [ + 'option:local', + jsonEncode({'name': 'input-source', 'value': value}) + ])); } Future mainGetMyId({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['my_id'])); } Future mainGetUuid({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['uuid'])); } Future mainGetPeerOption( @@ -887,11 +987,11 @@ class RustdeskImpl { } Future mainGetNewStoredPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewStoredPeers"); } Future mainForgetPassword({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['forget'])); + return mainSetPeerOption(id: id, key: 'password', value: ''); } Future mainPeerHasPassword({required String id, dynamic hint}) { @@ -920,7 +1020,7 @@ class RustdeskImpl { Future mainLoadRecentPeersForAb( {required String filter, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadRecentPeersForAb"); } Future mainLoadFavPeers({dynamic hint}) { @@ -928,31 +1028,32 @@ class RustdeskImpl { } Future mainLoadLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadLanPeers"); } Future mainRemoveDiscovered({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainRemoveDiscovered"); } Future mainChangeTheme({required String dark, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeTheme"); } Future mainChangeLanguage({required String lang, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeLanguage"); } String mainVideoSaveDirectory({required bool root, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVideoSaveDirectory"); } Future mainSetUserDefaultOption( {required String key, required String value, dynamic hint}) { - return js.context.callMethod('getByName', [ + js.context.callMethod('setByName', [ 'option:user:default', jsonEncode({'name': key, 'value': value}) ]); + return Future.value(); } String mainGetUserDefaultOption({required String key, dynamic hint}) { @@ -972,7 +1073,7 @@ class RustdeskImpl { } String mainGetDisplays({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDisplays"); } Future sessionAddPortForward( @@ -981,44 +1082,43 @@ class RustdeskImpl { required String remoteHost, required int remotePort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddPortForward"); } Future sessionRemovePortForward( {required UuidValue sessionId, required int localPort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRemovePortForward"); } Future sessionNewRdp({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionNewRdp"); } Future sessionRequestVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestVoiceCall"); } Future sessionCloseVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionCloseVoiceCall"); } Future cmHandleIncomingVoiceCall( {required int id, required bool accept, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmHandleIncomingVoiceCall"); } Future cmCloseVoiceCall({required int id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseVoiceCall"); } Future mainGetLastRemoteId({dynamic hint}) { - return Future( - () => js.context.callMethod('getByName', ['option', 'last_remote_id'])); + return Future(() => mainGetLocalOption(key: 'last_remote_id')); } - Future mainGetSoftwareUpdateUrl({dynamic hint}) { - throw UnimplementedError(); + Future mainGetSoftwareUpdateUrl({dynamic hint}) { + throw UnimplementedError("mainGetSoftwareUpdateUrl"); } Future mainGetHomeDir({dynamic hint}) { @@ -1026,7 +1126,7 @@ class RustdeskImpl { } Future mainGetLangs({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['langs'])); } Future mainGetTemporaryPassword({dynamic hint}) { @@ -1038,19 +1138,19 @@ class RustdeskImpl { } Future mainGetFingerprint({dynamic hint}) { - throw UnimplementedError(); + return Future.value(''); } Future cmGetClientsState({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClientsState"); } Future cmCheckClientsLength({required int length, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future cmGetClientsLength({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future mainInit({required String appDir, dynamic hint}) { @@ -1059,69 +1159,95 @@ class RustdeskImpl { Future mainDeviceId({required String id, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceId"); } Future mainDeviceName({required String name, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceName"); } Future mainRemovePeer({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['remove', id])); + return Future( + () => js.context.callMethod('setByName', ['remove_peer', id])); } bool mainHasHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasHwcodec"); } bool mainHasVram({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasVram"); } String mainSupportedHwdecodings({dynamic hint}) { - throw UnimplementedError(); + return '{}'; } Future mainIsRoot({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsRoot"); } int getDoubleClickTime({dynamic hint}) { - throw UnimplementedError(); + return 500; } Future mainStartDbusServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartDbusServer"); } Future mainSaveAb({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['save_ab', json])); } Future mainClearAb({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_ab'])); } Future mainLoadAb({dynamic hint}) { - throw UnimplementedError(); + Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load ab timed out')); + return 'Timeout'; + }, + ); + js.context["onLoadAbFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_ab']); + return timeoutFuture; } Future mainSaveGroup({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['save_group', json])); } Future mainClearGroup({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_group'])); } Future mainLoadGroup({dynamic hint}) { - throw UnimplementedError(); + Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load group timed out')); + return 'Timeout'; + }, + ); + js.context["onLoadGroupFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_group']); + return timeoutFuture; } Future sessionSendPointer( {required UuidValue sessionId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSendPointer"); } Future sessionSendMouse( @@ -1142,7 +1268,8 @@ class RustdeskImpl { Future sessionSendNote( {required UuidValue sessionId, required String note, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_note', note])); } Future sessionAlternativeCodecs( @@ -1167,81 +1294,82 @@ class RustdeskImpl { required int index, required bool on, dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'toggle_virtual_display', + jsonEncode({'index': index, 'on': on}) + ])); } Future mainSetHomeDir({required String home, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetHomeDir"); } String mainGetDataDirIos({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDataDirIos"); } Future mainStopService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStopService"); } Future mainStartService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartService"); } Future mainUpdateTemporaryPassword({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateTemporaryPassword"); } Future mainSetPermanentPassword( {required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetPermanentPassword"); } Future mainCheckSuperUserPermission({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckSuperUserPermission"); } Future mainCheckMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckMouseTime"); } Future mainGetMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetMouseTime"); } Future mainWol({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainWol"); } Future mainCreateShortcut({required String id, dynamic hint}) { - // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainCreateShortcut"); } Future cmSendChat( {required int connId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSendChat"); } Future cmLoginRes( {required int connId, required bool res, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmLoginRes"); } Future cmCloseConnection({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseConnection"); } Future cmRemoveDisconnectedConnection( {required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmRemoveDisconnectedConnection"); } Future cmCheckClickTime({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClickTime"); } Future cmGetClickTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClickTime"); } Future cmSwitchPermission( @@ -1249,28 +1377,27 @@ class RustdeskImpl { required String name, required bool enabled, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchPermission"); } bool cmCanElevate({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCanElevate"); } Future cmElevatePortable({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmElevatePortable"); } Future cmSwitchBack({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchBack"); } Future cmGetConfig({required String name, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetConfig"); } Future mainGetBuildDate({dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['build_date'])); } String translate( @@ -1319,89 +1446,89 @@ class RustdeskImpl { } bool mainIsInstalled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } void mainInitInputSource({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } bool mainIsInstalledLowerVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledLowerVersion"); } bool mainIsInstalledDaemon({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledDaemon"); } bool mainIsProcessTrusted({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsProcessTrusted"); } bool mainIsCanScreenRecording({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanScreenRecording"); } bool mainIsCanInputMonitoring({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanInputMonitoring"); } bool mainIsShareRdp({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsShareRdp"); } Future mainSetShareRdp({required bool enable, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetShareRdp"); } bool mainGotoInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGotoInstall"); } String mainGetNewVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewVersion"); } bool mainUpdateMe({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateMe"); } Future setCurSessionId({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("setCurSessionId"); } bool installShowRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installShowRunWithoutInstall"); } Future installRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installRunWithoutInstall"); } Future installInstallMe( {required String options, required String path, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallMe"); } String installInstallPath({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallPath"); } Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuth"); } Future mainAccountAuthCancel({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthCancel"); } Future mainAccountAuthResult({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthResult"); } Future mainOnMainWindowClose({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainOnMainWindowClose"); } bool mainCurrentIsWayland({dynamic hint}) { @@ -1412,8 +1539,8 @@ class RustdeskImpl { return false; } - bool mainHideDocker({dynamic hint}) { - throw UnimplementedError(); + bool mainHideDock({dynamic hint}) { + throw UnimplementedError("mainHideDock"); } bool mainHasFileClipboard({dynamic hint}) { @@ -1425,11 +1552,11 @@ class RustdeskImpl { } Future cmInit({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmInit"); } Future mainStartIpcUrlServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartIpcUrlServer"); } Future mainTestWallpaper({required int second, dynamic hint}) { @@ -1479,7 +1606,7 @@ class RustdeskImpl { } Future sendUrlScheme({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sendUrlScheme"); } Future pluginEvent( @@ -1487,12 +1614,12 @@ class RustdeskImpl { required String peer, required Uint8List event, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEvent"); } Stream pluginRegisterEventStream( {required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginRegisterEventStream"); } String? pluginGetSessionOption( @@ -1500,7 +1627,7 @@ class RustdeskImpl { required String peer, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSessionOption"); } Future pluginSetSessionOption( @@ -1509,12 +1636,12 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSessionOption"); } String? pluginGetSharedOption( {required String id, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSharedOption"); } Future pluginSetSharedOption( @@ -1522,36 +1649,36 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSharedOption"); } Future pluginReload({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginReload"); } void pluginEnable({required String id, required bool v, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEnable"); } bool pluginIsEnabled({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginIsEnabled"); } bool pluginFeatureIsEnabled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginFeatureIsEnabled"); } Future pluginSyncUi({required String syncTo, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSyncUi"); } Future pluginListReload({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginListReload"); } Future pluginInstall( {required String id, required bool b, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginInstall"); } bool isSupportMultiUiSession({required String version, dynamic hint}) { @@ -1563,63 +1690,162 @@ class RustdeskImpl { } String mainDefaultPrivacyModeImpl({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDefaultPrivacyModeImpl"); } String mainSupportedPrivacyModeImpls({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSupportedPrivacyModeImpls"); } String mainSupportedInputSource({dynamic hint}) { return jsonEncode([ + ['Input source 1', 'input_source_1_tip'], ['Input source 2', 'input_source_2_tip'] ]); } Future mainGenerate2Fa({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGenerate2Fa"); } Future mainVerify2Fa({required String code, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerify2Fa"); } bool mainHasValid2FaSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValid2FaSync"); } String mainGetHardOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHardOption"); } Future mainCheckHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckHwcodec"); } Future sessionRequestNewDisplayInitMsgs( {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestNewDisplayInitMsgs"); } Future mainHandleWaylandScreencastRestoreToken( {required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHandleWaylandScreencastRestoreToken"); } bool mainIsOptionFixed({required String key, dynamic hint}) { - throw UnimplementedError(); + return false; } bool mainGetUseTextureRender({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetUseTextureRender"); } bool mainHasValidBotSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValidBotSync"); } Future mainVerifyBot({required String token, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerifyBot"); + } + + String mainGetUnlockPin({dynamic hint}) { + throw UnimplementedError("mainGetUnlockPin"); + } + + String mainSetUnlockPin({required String pin, dynamic hint}) { + throw UnimplementedError("mainSetUnlockPin"); + } + + bool sessionGetEnableTrustedDevices( + {required UuidValue sessionId, dynamic hint}) { + return js.context.callMethod('getByName', ['enable_trusted_devices']) == + 'Y'; + } + + Future mainGetTrustedDevices({dynamic hint}) { + throw UnimplementedError("mainGetTrustedDevices"); + } + + Future mainRemoveTrustedDevices({required String json, dynamic hint}) { + throw UnimplementedError("mainRemoveTrustedDevices"); + } + + Future mainClearTrustedDevices({dynamic hint}) { + throw UnimplementedError("mainClearTrustedDevices"); + } + + Future getVoiceCallInputDevice({required bool isCm, dynamic hint}) { + throw UnimplementedError("getVoiceCallInputDevice"); + } + + Future setVoiceCallInputDevice( + {required bool isCm, required String device, dynamic hint}) { + throw UnimplementedError("setVoiceCallInputDevice"); + } + + bool isPresetPasswordMobileOnly({dynamic hint}) { + throw UnimplementedError("isPresetPasswordMobileOnly"); + } + + String mainGetBuildinOption({required String key, dynamic hint}) { + return ''; + } + + String installInstallOptions({dynamic hint}) { + throw UnimplementedError("installInstallOptions"); + } + + int mainMaxEncryptLen({dynamic hint}) { + throw UnimplementedError("mainMaxEncryptLen"); + } + + bool mainAudioSupportLoopback({dynamic hint}) { + return false; + } + + Future sessionReadLocalEmptyDirsRecursiveSync( + {required UuidValue sessionId, + required String path, + required bool includeHidden, + dynamic hint}) { + throw UnimplementedError("sessionReadLocalEmptyDirsRecursiveSync"); + } + + Future sessionReadRemoteEmptyDirsRecursiveSync( + {required UuidValue sessionId, + required String path, + required bool includeHidden, + dynamic hint}) { + throw UnimplementedError("sessionReadRemoteEmptyDirsRecursiveSync"); + } + + Future sessionRenameFile( + {required UuidValue sessionId, + required int actId, + required String path, + required String newName, + required bool isRemote, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'rename_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'new_name': newName, + 'is_remote': isRemote + }) + ])); + } + + Future sessionSelectFiles( + {required UuidValue sessionId, dynamic hint}) { + return Future(() => js.context.callMethod('setByName', ['select_files'])); + } + + String? sessionGetConnToken({required UuidValue sessionId, dynamic hint}) { + throw UnimplementedError("sessionGetConnToken"); } void dispose() {} diff --git a/flutter/lib/web/common.dart b/flutter/lib/web/common.dart index 93b53f948021..4d539d5d47cc 100644 --- a/flutter/lib/web/common.dart +++ b/flutter/lib/web/common.dart @@ -1,4 +1,7 @@ import 'dart:js' as js; +import 'dart:html' as html; +// cycle imports, maybe we can improve this +import 'package:flutter_hbb/consts.dart'; final isAndroid_ = false; final isIOS_ = false; @@ -11,3 +14,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile'); final isDesktop_ = false; String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); + +final _localOs = js.context.callMethod('getByName', ['local_os', '']); +final isWebOnWindows_ = _localOs == kPeerPlatformWindows; +final isWebOnLinux_ = _localOs == kPeerPlatformLinux; +final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS; diff --git a/flutter/lib/web/custom_cursor.dart b/flutter/lib/web/custom_cursor.dart index fd1fc4a18f85..54df77e98b5e 100644 --- a/flutter/lib/web/custom_cursor.dart +++ b/flutter/lib/web/custom_cursor.dart @@ -58,6 +58,11 @@ class CursorManager { ]); } } + + Future resetSystemCursor() async { + latestKey = ''; + js.context.callMethod('setByName', ['cursor', 'auto']); + } } class FlutterCustomMemoryImageCursor extends MouseCursor { @@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession { } deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); +resetSystemCursor() => CursorManager.instance.resetSystemCursor(); MouseCursor buildCursorOfCache( model.CursorModel cursor, double scale, model.CursorData? cache) { diff --git a/flutter/lib/web/dummy.dart b/flutter/lib/web/dummy.dart new file mode 100644 index 000000000000..b9e3b80b6ed4 --- /dev/null +++ b/flutter/lib/web/dummy.dart @@ -0,0 +1,14 @@ +Future webselectFiles({required bool is_folder}) async { + throw UnimplementedError("webselectFiles"); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + throw UnimplementedError("webSendLocalFiles"); +} diff --git a/flutter/lib/web/settings_page.dart b/flutter/lib/web/settings_page.dart new file mode 100644 index 000000000000..1cf23ecf9f88 --- /dev/null +++ b/flutter/lib/web/settings_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; + +class WebSettingsPage extends StatelessWidget { + const WebSettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return _buildDesktopButton(context); + } + + Widget _buildDesktopButton(BuildContext context) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + DesktopSettingPage(initialTabkey: SettingsTabKey.general), + ), + ); + }, + ); + } +} diff --git a/flutter/lib/web/texture_rgba_renderer.dart b/flutter/lib/web/texture_rgba_renderer.dart index 83407773583a..9a4a1879b12d 100644 --- a/flutter/lib/web/texture_rgba_renderer.dart +++ b/flutter/lib/web/texture_rgba_renderer.dart @@ -6,7 +6,7 @@ class TextureRgbaRenderer { } Future closeTexture(int key) { - throw UnimplementedError(); + return Future(() => true); } Future onRgba( diff --git a/flutter/lib/web/web_unique.dart b/flutter/lib/web/web_unique.dart new file mode 100644 index 000000000000..14774e668b0c --- /dev/null +++ b/flutter/lib/web/web_unique.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js' as js; + +Future webselectFiles({required bool is_folder}) async { + return Future( + () => js.context.callMethod('setByName', ['select_files', is_folder])); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_local_files', + jsonEncode({ + 'id': actId, + 'handle_index': handleIndex, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + }) + ])); +} diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 56b85ccae6dd..b9d36a0ce8d9 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -14,8 +14,13 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +GtkWidget *find_gl_area(GtkWidget *widget); +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); + extern bool gIsConnectionManager; +GtkWidget *find_gl_area(GtkWidget *widget); + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -39,9 +44,10 @@ static void my_application_activate(GApplication* application) { // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; + GdkScreen* screen = NULL; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { + screen = gtk_window_get_screen(window); + if (screen != NULL && GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; @@ -66,16 +72,19 @@ static void my_application_activate(GApplication* application) { height = 490; } gtk_window_set_default_size(window, width, height); // <-- comment this line - gtk_widget_show(GTK_WIDGET(window)); + // gtk_widget_show(GTK_WIDGET(window)); gtk_widget_set_opacity(GTK_WIDGET(window), 0); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + try_set_transparent(window, gtk_window_get_screen(window), view); + gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_show(GTK_WIDGET(view)); + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -121,3 +130,48 @@ MyApplication* my_application_new() { "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } + +GtkWidget *find_gl_area(GtkWidget *widget) +{ + if (GTK_IS_GL_AREA(widget)) { + return widget; + } + + if (GTK_IS_CONTAINER(widget)) { + GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); + for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) { + GtkWidget *child = GTK_WIDGET(iter->data); + GtkWidget *gl_area = find_gl_area(child); + if (gl_area != NULL) { + g_list_free(children); + return gl_area; + } + } + g_list_free(children); + } + + return NULL; +} + +// https://github.com/flutter/flutter/issues/152154 +// Remove this workaround when flutter version is updated. +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) +{ + GtkWidget *gl_area = NULL; + + printf("Try setting transparent\n"); + + gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } +} diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 3498decd37a5..46372a5822fc 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/Info.plist b/flutter/macos/Runner/Info.plist index d8f9db36ce61..36ca81e18c05 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -45,5 +45,7 @@ Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass NSApplication + LSUIElement + 1 diff --git a/flutter/ndk_x86.sh b/flutter/ndk_x86.sh new file mode 100755 index 000000000000..617c25f65de4 --- /dev/null +++ b/flutter/ndk_x86.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo ndk --platform 21 --target i686-linux-android build --release --features flutter diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 060606181bbb..7a2b861c8573 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363" + resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -377,9 +377,25 @@ packages: path: "." ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" - url: "https://github.com/21pages/dynamic_layouts.git" + url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + extended_text: + dependency: "direct main" + description: + name: extended_text + sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676" + url: "https://pub.dev" + source: hosted + version: "14.0.0" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + url: "https://pub.dev" + source: hosted + version: "12.0.0" external_path: dependency: "direct main" description: @@ -392,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" ffigen: dependency: "direct dev" description: @@ -509,20 +525,11 @@ packages: dependency: "direct main" description: path: "." - ref: "38951317afe79d953ab25733667bd96e172a80d3" - resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" - url: "https://github.com/21pages/flutter_gpu_texture_renderer" + ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" - flutter_improved_scrolling: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" - url: "https://github.com/rustdesk-org/flutter_improved_scrolling" - source: git - version: "0.0.3" flutter_keyboard_visibility: dependency: "direct main" description: @@ -729,10 +736,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "1.1.2" image_picker_android: dependency: transitive description: @@ -777,10 +784,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.10.0" image_picker_windows: dependency: transitive description: @@ -849,18 +856,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1021,14 +1028,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -1269,10 +1268,11 @@ packages: texture_rgba_renderer: dependency: "direct main" description: - name: texture_rgba_renderer - sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61 - url: "https://pub.dev" - source: hosted + path: "." + ref: "42797e0f03141dc2b585f76c64a13974508058b4" + resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" + url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer" + source: git version: "0.0.16" timing: dependency: transitive @@ -1309,10 +1309,11 @@ packages: uni_links: dependency: "direct main" description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + url: "https://github.com/rustdesk-org/uni_links" + source: git version: "0.5.1" uni_links_desktop: dependency: "direct main" @@ -1350,26 +1351,26 @@ 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: transitive + dependency: "direct main" description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.2" url_launcher_linux: dependency: transitive description: @@ -1542,24 +1543,24 @@ packages: dependency: "direct main" description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" window_manager: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: f19acdb008645366339444a359a45c3257c8b32e + resolved-ref: "85789bfe6e4cfaf4ecc00c52857467fdb7f26879" url: "https://github.com/rustdesk-org/window_manager" source: git version: "0.3.6" @@ -1567,8 +1568,8 @@ packages: dependency: "direct main" description: path: "plugins/window_size" - ref: a738913c8ce2c9f47515382d40827e794a334274 - resolved-ref: a738913c8ce2c9f47515382d40827e794a334274 + ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 + resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 url: "https://github.com/google/flutter-desktop-embedding.git" source: git version: "0.1.0" @@ -1613,5 +1614,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c6e1e61cc5fa..3871449996e0 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.2.7+46 +version: 1.3.7+56 environment: sdk: '^3.1.0' @@ -35,7 +35,8 @@ 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: git: @@ -46,7 +47,7 @@ dependencies: http: ^1.1.0 qr_code_scanner: ^1.0.0 zxing2: ^0.2.0 - image_picker: ^0.8.5 + image_picker: ^1.1.2 image: ^4.0.17 back_button_interceptor: ^6.0.1 flutter_rust_bridge: "1.80.1" @@ -62,7 +63,7 @@ dependencies: git: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 + ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 get: ^4.6.5 visibility_detector: ^0.4.0+2 contextmenu: ^3.0.0 @@ -71,14 +72,11 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^2.0.5 - flutter_improved_scrolling: - # currently, we use flutter 3.10.0+. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + uni_links: git: - url: https://github.com/rustdesk-org/flutter_improved_scrolling - uni_links: ^0.5.1 + url: https://github.com/rustdesk-org/uni_links + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works path: ^1.8.1 auto_size_text: ^3.0.0 @@ -87,23 +85,27 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.13.1 flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.16 + texture_rgba_renderer: + git: + url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer + ref: 42797e0f03141dc2b585f76c64a13974508058b4 percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 flutter_gpu_texture_renderer: git: - url: https://github.com/21pages/flutter_gpu_texture_renderer - ref: 38951317afe79d953ab25733667bd96e172a80d3 + url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer + ref: 2ded7f146437a761ffe6981e2f742038f85ca68d uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 dynamic_layouts: git: - url: https://github.com/21pages/dynamic_layouts.git + url: https://github.com/rustdesk-org/dynamic_layouts.git ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9 pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 + extended_text: 14.0.0 dev_dependencies: icons_launcher: ^2.0.4 @@ -185,3 +187,4 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + diff --git a/libs/clipboard/docs/assets/win_A_B.png b/libs/clipboard/docs/assets/win_A_B.png index 87fe702dd9b6..9281c20dc630 100644 Binary files a/libs/clipboard/docs/assets/win_A_B.png and b/libs/clipboard/docs/assets/win_A_B.png differ diff --git a/libs/clipboard/docs/assets/win_B_A.png b/libs/clipboard/docs/assets/win_B_A.png index e4c9f3246018..a80cd8246dd8 100644 Binary files a/libs/clipboard/docs/assets/win_B_A.png and b/libs/clipboard/docs/assets/win_B_A.png differ diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 1a9a047578f8..6bdd2293aa63 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -5,7 +5,7 @@ use std::{ }; #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, log}; +use hbb_common::{allow_err, bail}; use hbb_common::{ lazy_static, tokio::sync::{ @@ -25,6 +25,8 @@ pub use context_send::*; const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; #[cfg(target_os = "windows")] const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; +#[cfg(target_os = "windows")] +const ERR_CODE_SEND_MSG: u32 = 0x00000003; pub(crate) use platform::create_cliprdr_context; @@ -105,6 +107,7 @@ pub enum ClipboardFile { stream_id: i32, requested_data: Vec, }, + TryEmpty, } struct MsgChannel { @@ -130,7 +133,7 @@ impl ClipboardFile { ) } - pub fn is_stopping_allowed_from_peer(&self) -> bool { + pub fn is_beginning_message(&self) -> bool { matches!( self, ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } @@ -198,7 +201,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] @@ -210,25 +213,38 @@ fn send_data(conn_id: i32, data: ClipboardFile) { } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { - // no need to handle result here +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - allow_err!(msg_channel.sender.send(data)); + msg_channel.sender.send(data)?; + Ok(()) + } else { + bail!("conn_id not found"); + } +} + +#[cfg(target_os = "windows")] +pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { + use hbb_common::log; + 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) { - // no need to handle result here +fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { + // 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 2be4ce809da0..5db271129734 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::{CliprdrError, CliprdrServiceContext}; #[cfg(target_os = "windows")] @@ -63,8 +64,10 @@ pub fn create_cliprdr_context( 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(()) diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index e24712efa4d3..b609b8cc79ef 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -3,7 +3,7 @@ use std::{ fs::File, io::{BufRead, BufReader, Read, Seek}, os::unix::prelude::PermissionsExt, - path::PathBuf, + path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; @@ -51,7 +51,7 @@ pub(super) struct LocalFile { } impl LocalFile { - pub fn try_open(path: &PathBuf) -> Result { + pub fn try_open(path: &Path) -> Result { let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { path: path.clone(), err: e, @@ -219,7 +219,7 @@ impl LocalFile { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { fn constr_file_lst( - path: &PathBuf, + path: &Path, file_list: &mut Vec, visited: &mut HashSet, ) -> Result<(), CliprdrError> { diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 9a086109473a..34021d6bf209 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,5 +1,5 @@ use std::{ - path::PathBuf, + path::{Path, PathBuf}, sync::{mpsc::Sender, Arc}, time::Duration, }; @@ -74,7 +74,7 @@ trait SysClipboard: Send + Sync { } #[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { +fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { #[cfg(feature = "wayland")] { unimplemented!() @@ -88,7 +88,7 @@ fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, Cli } #[cfg(target_os = "macos")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { +fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { use ns_clipboard::*; let ns_pb = NsPasteboard::new(ignore_path)?; Ok(Box::new(ns_pb) as Box<_>) diff --git a/libs/clipboard/src/platform/unix/ns_clipboard.rs b/libs/clipboard/src/platform/unix/ns_clipboard.rs index 32c60a4643f8..a9112fe62591 100644 --- a/libs/clipboard/src/platform/unix/ns_clipboard.rs +++ b/libs/clipboard/src/platform/unix/ns_clipboard.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeSet, path::PathBuf}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; use cacao::pasteboard::{Pasteboard, PasteboardName}; use hbb_common::log; @@ -30,7 +33,7 @@ pub struct NsPasteboard { } impl NsPasteboard { - pub fn new(ignore_path: &PathBuf) -> Result { + pub fn new(ignore_path: &Path) -> Result { Ok(Self { ignore_path: ignore_path.to_owned(), former_file_list: Mutex::new(vec![]), diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs index 2ae520f4dfcd..126a341cd728 100644 --- a/libs/clipboard/src/platform/unix/url.rs +++ b/libs/clipboard/src/platform/unix/url.rs @@ -7,7 +7,7 @@ use crate::CliprdrError; // 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 { +pub(super) fn encode_path_to_uri(path: &Path) -> io::Result { let encoded = percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string(); format!("file://{}", encoded) diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs index 41b642640444..606ff6719961 100644 --- a/libs/clipboard/src/platform/unix/x11.rs +++ b/libs/clipboard/src/platform/unix/x11.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeSet, path::PathBuf}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; use hbb_common::log; use once_cell::sync::OnceCell; @@ -26,7 +29,7 @@ pub struct X11Clipboard { } impl X11Clipboard { - pub fn new(ignore_path: &PathBuf) -> Result { + pub fn new(ignore_path: &Path) -> Result { let clipboard = get_clip()?; let text_uri_list = clipboard .setter diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 8fc917c6fee4..3b931a4a6516 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -6,10 +6,10 @@ #![allow(deref_nullptr)] use crate::{ - allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, - ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, + send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, 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}, @@ -643,6 +643,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 +741,11 @@ 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 } @@ -998,7 +1004,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) } }; // no need to handle result here - send_data(conn_id as _, data); + allow_err!(send_data(conn_id as _, data)); 0 } @@ -1045,7 +1051,13 @@ extern "C" fn client_format_list( .iter() .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); } else { - send_data(conn_id, data); + match send_data(conn_id, data) { + Ok(_) => {} + Err(e) => { + log::error!("failed to send format list: {:?}", e); + return ERR_CODE_SEND_MSG; + } + } } 0 @@ -1067,9 +1079,13 @@ extern "C" fn client_format_list_response( msg_flags ); let data = ClipboardFile::FormatListResponse { msg_flags }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format list response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_request( @@ -1090,10 +1106,13 @@ extern "C" fn client_format_data_request( conn_id, requested_format_id ); - // no need to handle result here - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_response( @@ -1125,9 +1144,13 @@ extern "C" fn client_format_data_response( msg_flags, format_data, }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_request( @@ -1175,9 +1198,13 @@ extern "C" fn client_file_contents_request( clip_data_id, }; log::debug!("client_file_contents_request called, data: {:?}", &data); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_response( @@ -1213,7 +1240,11 @@ extern "C" fn client_file_contents_response( msg_flags, stream_id ); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index c8f2038a1d28..e065be215c38 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -211,6 +211,11 @@ struct wf_clipboard BOOL sync; UINT32 capabilities; + // This flag is not really needed, + // but we can use it to double confirm that files can only be pasted after `Ctrl+C`. + // Not sure `is_file_descriptor_from_remote()` is engough to check all cases on all Windows. + BOOL copied; + size_t map_size; size_t map_capacity; formatMapping *format_mappings; @@ -220,7 +225,8 @@ struct wf_clipboard HWND hwnd; HANDLE hmem; HANDLE thread; - HANDLE response_data_event; + HANDLE formatDataRespEvent; + BOOL formatDataRespReceived; LPDATAOBJECT data_obj; HANDLE data_obj_mutex; @@ -228,6 +234,7 @@ struct wf_clipboard ULONG req_fsize; char *req_fdata; HANDLE req_fevent; + BOOL req_f_received; size_t nFiles; size_t file_array_size; @@ -261,6 +268,9 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con ULONG index, UINT32 flag, DWORD positionhigh, 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); static CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc); @@ -287,6 +297,9 @@ static BOOL try_open_clipboard(HWND hwnd) static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, void **ppvObject) { + if (ppvObject == NULL) + return E_INVALIDARG; + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) { IStream_AddRef(This); @@ -362,6 +375,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO } *pcbRead = clipboard->req_fsize; + // Check overflow, can not be a real case + if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) { + // It's better to crash to release the explorer.exe + // This is a critical error, because the explorer is waiting for the data + // and the m_lOffset is wrong(overflowed) + return S_FALSE; + } instance->m_lOffset.QuadPart += clipboard->req_fsize; if (clipboard->req_fsize < cb) @@ -517,11 +537,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) { - IStream *iStream; + IStream *iStream = NULL; BOOL success = FALSE; BOOL isDir = FALSE; - CliprdrStream *instance; + CliprdrStream *instance = NULL; wfClipboard *clipboard = (wfClipboard *)pData; + + if (!(pData && dsc)) + { + return NULL; + } + instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); if (instance) @@ -575,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; + } } } @@ -694,6 +723,15 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO if (!clipboard) return E_INVALIDARG; + // If `Ctrl+C` is not pressed yet, do not handle the file paste, and empty the clipboard. + if (!clipboard->copied) { + if (try_open_clipboard(clipboard->hwnd)) { + EmptyClipboard(); + CloseClipboard(); + } + return E_UNEXPECTED; + } + if ((idx = cliprdr_lookup_format(instance, pFormatEtc)) == -1) { // empty clipboard here? @@ -874,14 +912,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, void *data) { - CliprdrDataObject *instance; - IDataObject *iDataObject; + CliprdrDataObject *instance = NULL; + IDataObject *iDataObject = NULL; instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); if (!instance) goto error; + instance->m_pFormatEtc = NULL; + instance->m_pStgMedium = NULL; + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = NULL; iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); if (!iDataObject->lpVtbl) @@ -929,7 +971,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc return instance; error: - CliprdrDataObject_Delete(instance); + if (iDataObject && iDataObject->lpVtbl) + { + free(iDataObject->lpVtbl); + } + if (instance) + { + if (instance->m_pFormatEtc) + { + free(instance->m_pFormatEtc); + } + + if (instance->m_pStgMedium) + { + free(instance->m_pStgMedium); + } + + CliprdrDataObject_Delete(instance); + } return NULL; } @@ -1010,6 +1069,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT REFIID riid, void **ppvObject) { (void)This; + if (!ppvObject) + return E_INVALIDARG; if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) { @@ -1198,6 +1259,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f WCHAR *unicode_name; #if !defined(UNICODE) size_t size; + int towchar_count; #endif if (!clipboard || !format_name) @@ -1205,6 +1267,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f #if defined(UNICODE) unicode_name = _wcsdup(format_name); + if (!unicode_name) + return 0; #else size = _tcslen(format_name); unicode_name = calloc(size + 1, sizeof(WCHAR)); @@ -1212,11 +1276,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f if (!unicode_name) return 0; - MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); -#endif - - if (!unicode_name) + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0); + if (towchar_count <= 0 || towchar_count > size) + return 0; + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); + if (towchar_count <= 0) return 0; +#endif for (i = 0; i < clipboard->map_size; i++) { @@ -1312,6 +1378,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard) if (!clipboard) return -1; + // to-do: + // Directly use the environment variable `TEMP` is not safe. + // But this function is not used for now. if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == 0) return -1; @@ -1430,6 +1499,8 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) // send rc = clipboard->context->ClientFormatList(clipboard->context, &formatList); + // No need to check `rc`, `copied` is only used to indicate `Ctrl+C` is pressed. + clipboard->copied = TRUE; for (index = 0; index < numFormats; index++) { @@ -1444,7 +1515,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) return rc; } -UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data) +// Ensure the event is not signaled, and reset it if it is. +UINT try_reset_event(HANDLE event) +{ + if (!event) + { + return ERROR_INTERNAL_ERROR; + } + + DWORD result = WaitForSingleObject(event, 0); + if (result == WAIT_OBJECT_0) + { + if (!ResetEvent(event)) + { + return GetLastError(); + } + else + { + return ERROR_SUCCESS; + } + } + else if (result == WAIT_TIMEOUT) + { + return ERROR_SUCCESS; + } + else + { + return ERROR_INTERNAL_ERROR; + } +} + +UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data) { UINT rc = ERROR_SUCCESS; clipboard->context->IsStopped = FALSE; @@ -1456,7 +1557,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) { - continue; + if ((*recvedFlag) == TRUE) { + // The data has been received, but the event is still not signaled. + // We just skip the rest of the waiting and reset the flag. + *recvedFlag = FALSE; + // Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data. + waitRes = WAIT_OBJECT_0; + } else { + // The data has not been received yet, we should continue to wait. + continue; + } + } + + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better } if (clipboard->context->IsStopped == TRUE) @@ -1470,12 +1585,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo return ERROR_INTERNAL_ERROR; } - if (!ResetEvent(event)) - { - // NOTE: critical error here, crash may be better - rc = ERROR_INTERNAL_ERROR; - } - if ((*data) == NULL) { rc = ERROR_INTERNAL_ERROR; @@ -1519,6 +1628,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->formatDataRespEvent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->formatDataRespReceived = FALSE; + remoteFormatId = get_remote_format_id(clipboard, formatId); formatDataRequest.connID = connID; @@ -1530,7 +1646,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN return rc; } - wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem); + return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem); } UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, @@ -1543,7 +1659,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->req_fevent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->req_f_received = FALSE; + fileContentsRequest.connID = connID; + // streamId is `IStream*` pointer, though it is not very good on a 64-bit system. + // But it is OK, because it is only used to check if the stream is the same in + // `wf_cliprdr_server_file_contents_request()` function. fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; fileContentsRequest.listIndex = index; fileContentsRequest.dwFlags = flag; @@ -1558,7 +1684,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co return rc; } - return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata); + return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata); } static UINT cliprdr_send_response_filecontents( @@ -1623,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) { @@ -1788,6 +1913,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM break; case WM_DESTROYCLIPBOARD: + // to-do: clear clipboard data? case WM_ASKCBFORMATNAME: case WM_HSCROLLCLIPBOARD: case WM_PAINTCLIPBOARD: @@ -1904,7 +2030,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po LONG positionHigh, DWORD nRequested, DWORD *puSize) { BOOL res = FALSE; - HANDLE hFile; + HANDLE hFile = NULL; DWORD nGet, rc; if (!file_name || !buffer || !puSize) @@ -1932,9 +2058,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po res = TRUE; error: - - if (!CloseHandle(hFile)) - res = FALSE; + if (hFile) + { + if (!CloseHandle(hFile)) + res = FALSE; + } if (res) *puSize = nGet; @@ -1945,8 +2073,8 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po /* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) { - HANDLE hFile; - FILEDESCRIPTORW *fd; + HANDLE hFile = NULL; + FILEDESCRIPTORW *fd = NULL; fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); if (!fd) @@ -1961,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); @@ -1975,7 +2105,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t } fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); - wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); + if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0])) + { + // The file name is too long, which is not a normal case. + // So we just return NULL. + CloseHandle(hFile); + free(fd); + return NULL; + } + + wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1); CloseHandle(hFile); return fd; @@ -2024,7 +2163,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi if (!clipboard->file_names[clipboard->nFiles]) return FALSE; - wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); + // `MAX_PATH` is long enough for the file name. + // So we just return FALSE if the file name is too long, which is not a normal case. + if ((wcslen(full_file_name) + 1) > MAX_PATH) + return FALSE; + + wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1); /* add to descriptor array */ clipboard->fileDescriptor[clipboard->nFiles] = wf_cliprdr_get_file_descriptor(full_file_name, pathLen); @@ -2048,8 +2192,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if (!clipboard || !Dir) return FALSE; - // StringCchCopy(DirSpec, MAX_PATH, Dir); - // StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); + if (wcslen(Dir) + 3 > MAX_PATH) + return FALSE; StringCchCopyW(DirSpec, MAX_PATH, Dir); StringCchCatW(DirSpec, MAX_PATH, L"\\*"); @@ -2078,9 +2222,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { WCHAR DirAdd[MAX_PATH]; - // StringCchCopy(DirAdd, MAX_PATH, Dir); - // StringCchCat(DirAdd, MAX_PATH, _T("\\")); - // StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(DirAdd, MAX_PATH, Dir); StringCchCatW(DirAdd, MAX_PATH, L"\\"); StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); @@ -2094,10 +2237,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si else { WCHAR fileName[MAX_PATH]; - // StringCchCopy(fileName, MAX_PATH, Dir); - // StringCchCat(fileName, MAX_PATH, _T("\\")); - // StringCchCat(fileName, MAX_PATH, FindFileData.cFileName); - + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(fileName, MAX_PATH, Dir); StringCchCatW(fileName, MAX_PATH, L"\\"); StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); @@ -2156,7 +2297,9 @@ static UINT wf_cliprdr_monitor_ready(CliprdrClientContext *context, if (rc != CHANNEL_RC_OK) return rc; - return cliprdr_send_format_list(clipboard, monitorReady->connID); + return rc; + // Don't send format list here, because we don't want to paste files copied before the connection. + // return cliprdr_send_format_list(clipboard, monitorReady->connID); } /** @@ -2203,11 +2346,20 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, UINT32 i; formatMapping *mapping; CLIPRDR_FORMAT *format; - wfClipboard *clipboard = (wfClipboard *)context->Custom; + wfClipboard *clipboard = NULL; + + if (!context || !formatList) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + return ERROR_INTERNAL_ERROR; if (!clear_format_map(clipboard)) return ERROR_INTERNAL_ERROR; + clipboard->copied = TRUE; + for (i = 0; i < formatList->numFormats; i++) { format = &(formatList->formats[i]); @@ -2242,9 +2394,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, if (context->EnableFiles) { UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); - *p_conn_id = formatList->connID; - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) - rc = CHANNEL_RC_OK; + if (p_conn_id) { + *p_conn_id = formatList->connID; + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) + rc = CHANNEL_RC_OK; + } } else { @@ -2265,16 +2419,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); - format_ids->connID = formatList->connID; - format_ids->size = (UINT32)clipboard->map_size; - format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); - for (i = 0; i < format_ids->size; ++i) + if (format_ids) { - format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; - } - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) - { - rc = CHANNEL_RC_OK; + format_ids->connID = formatList->connID; + format_ids->size = (UINT32)clipboard->map_size; + format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); + if (format_ids->formats) + { + for (i = 0; i < format_ids->size; ++i) + { + format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; + } + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) + { + rc = CHANNEL_RC_OK; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } } else { @@ -2469,17 +2637,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, p += len + 1, clipboard->nFiles++) { int cchWideChar; - WCHAR *wFileName; cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); - MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); - wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + if (wFileName) + { + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + free(wFileName); + } + else + { + rc = ERROR_INTERNAL_ERROR; + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + goto exit; + } } } GlobalUnlock(stg_medium.hGlobal); ReleaseStgMedium(&stg_medium); resp: + // size will not overflow, because size type is size_t (unsigned __int64) size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); @@ -2519,10 +2698,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, globlemem = (char *)GlobalLock(hClipdata); size = (int)GlobalSize(hClipdata); buff = malloc(size); - CopyMemory(buff, globlemem, size); + if (buff) + { + CopyMemory(buff, globlemem, size); + rc = ERROR_SUCCESS; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } GlobalUnlock(hClipdata); CloseClipboard(); - rc = ERROR_SUCCESS; } } else @@ -2545,7 +2731,7 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, response.requestedFormatData = (BYTE *)buff; if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) { - // CAUTION: if failed to send, server will wait a long time + // CAUTION: if failed to send, server will wait a long time, default 30 seconds. } if (buff) @@ -2621,9 +2807,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context, rc = CHANNEL_RC_OK; } while (0); - if (!SetEvent(clipboard->response_data_event)) + if (!SetEvent(clipboard->formatDataRespEvent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->formatDataRespReceived = TRUE; rc = ERROR_INTERNAL_ERROR; } return rc; @@ -2666,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); @@ -2899,11 +3112,34 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, if (!SetEvent(clipboard->req_fevent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->req_f_received = TRUE; } 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; + if (IsClipboardFormatAvailable(CF_HDROP)) { + return FALSE; + } + fsid = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + if (IsClipboardFormatAvailable(fsid)) { + return TRUE; + } + return FALSE; +} + BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) { if (!clipboard || !cliprdr) @@ -2915,6 +3151,7 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) clipboard->map_size = 0; clipboard->hUser32 = LoadLibraryA("user32.dll"); clipboard->data_obj = NULL; + clipboard->copied = FALSE; if (clipboard->hUser32) { @@ -2934,14 +3171,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) goto error; - if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->formatDataRespReceived = FALSE; if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) goto error; if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->req_f_received = FALSE; if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) goto error; @@ -2968,14 +3207,18 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) if (!clipboard || !cliprdr) return FALSE; + clipboard->copied = FALSE; cliprdr->Custom = NULL; /* discard all contexts in clipboard */ if (try_open_clipboard(clipboard->hwnd)) { - if (!EmptyClipboard()) + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { - DEBUG_CLIPRDR("EmptyClipboard failed with 0x%x", GetLastError()); + if (!EmptyClipboard()) + { + DEBUG_CLIPRDR("EmptyClipboard failed with 0x%x", GetLastError()); + } } if (!CloseClipboard()) { @@ -3002,8 +3245,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) clipboard->data_obj = NULL; } - if (clipboard->response_data_event) - CloseHandle(clipboard->response_data_event); + if (clipboard->formatDataRespEvent) + CloseHandle(clipboard->formatDataRespEvent); if (clipboard->data_obj_mutex) CloseHandle(clipboard->data_obj_mutex); @@ -3069,6 +3312,8 @@ BOOL wf_do_empty_cliprdr(wfClipboard *clipboard) return FALSE; } + clipboard->copied = FALSE; + if (WaitForSingleObject(clipboard->data_obj_mutex, INFINITE) != WAIT_OBJECT_0) { return FALSE; @@ -3090,10 +3335,14 @@ BOOL wf_do_empty_cliprdr(wfClipboard *clipboard) break; } - if (!EmptyClipboard()) + if (is_file_descriptor_from_remote()) { - rc = FALSE; + if (!EmptyClipboard()) + { + rc = FALSE; + } } + if (!CloseClipboard()) { // critical error!!! diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 1549209e13c0..a5b6d5622eda 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" rdev = { git = "https://github.com/rustdesk-org/rdev" } -tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller" } +tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" } hbb_common = { path = "../hbb_common" } [features] diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index c082236e3f5d..902d77948e28 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -345,7 +345,7 @@ fn convert_to_tfc_key(key: Key) -> Option { Key::Numpad9 => TFC_Key::N9, Key::Decimal => TFC_Key::NumpadDecimal, Key::Clear => TFC_Key::NumpadClear, - Key::Pause => TFC_Key::PlayPause, + Key::Pause => TFC_Key::Pause, Key::Print => TFC_Key::Print, Key::Snapshot => TFC_Key::PrintScreen, Key::Insert => TFC_Key::Insert, diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index b56beff129f7..e7d7d9e8d338 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3; const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; const BUF_LEN: usize = 4; +const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3; +const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4; + /// The event source user data value of cgevent. pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100; @@ -108,11 +111,17 @@ pub struct Enigo { double_click_interval: u32, last_click_time: Option, multiple_click: i64, + ignore_flags: bool, flags: CGEventFlags, char_to_vkey_map: Map>, } impl Enigo { + /// Set if ignore flags when posting events. + pub fn set_ignore_flags(&mut self, ignore: bool) { + self.ignore_flags = ignore; + } + /// pub fn reset_flag(&mut self) { self.flags = CGEventFlags::CGEventFlagNull; @@ -133,7 +142,9 @@ impl Enigo { } fn post(&self, event: CGEvent) { - event.set_flags(self.flags); + if !self.ignore_flags { + event.set_flags(self.flags); + } event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); event.post(CGEventTapLocation::HID); } @@ -161,6 +172,7 @@ impl Default for Enigo { double_click_interval, multiple_click: 1, last_click_time: None, + ignore_flags: false, flags: CGEventFlags::CGEventFlagNull, char_to_vkey_map: Default::default(), } @@ -226,14 +238,24 @@ impl MouseControllable for Enigo { } self.last_click_time = Some(now); let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return Ok(()); - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -244,6 +266,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -252,14 +277,24 @@ impl MouseControllable for Enigo { fn mouse_up(&mut self, button: MouseButton) { let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return; - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -270,6 +305,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -345,7 +383,7 @@ impl KeyboardControllable for Enigo { fn as_mut_any(&mut self) -> &mut dyn std::any::Any { self } - + fn key_sequence(&mut self, sequence: &str) { // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time @@ -382,12 +420,10 @@ impl KeyboardControllable for Enigo { fn key_down(&mut self, key: Key) -> crate::ResultType { let code = self.key_to_keycode(key); if code == u16::MAX { - return Err("".into()); + return Err("".into()); } if let Some(src) = self.event_source.as_ref() { - if let Ok(event) = - CGEvent::new_keyboard_event(src.clone(), code, true) - { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) { self.post(event); } } diff --git a/libs/hbb_common b/libs/hbb_common new file mode 160000 index 000000000000..97266d7c180f --- /dev/null +++ b/libs/hbb_common @@ -0,0 +1 @@ +Subproject commit 97266d7c180feef8b43726ea6fcb4491e3fd8752 diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore deleted file mode 100644 index 693699042b1a..000000000000 --- 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 7aeb43797764..000000000000 --- 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"] } -# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 -sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } -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/21pages/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 5ebc3a28706f..000000000000 --- 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 95169df8e2cb..000000000000 --- 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 0be788428681..000000000000 --- 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 f346a72288c0..000000000000 --- a/libs/hbb_common/protos/message.proto +++ /dev/null @@ -1,820 +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; -} - -message Auth2FA { - string code = 1; -} - -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; - } -} - -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 { - bool down = 1; - 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; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; - int32 width = 3; - int32 height = 4; -} - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; - bool is_identical = 6; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; - 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; - } -} diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto deleted file mode 100644 index 2fc0d9040e6e..000000000000 --- 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 bfc79871554a..000000000000 --- 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 761d916e4f83..000000000000 --- 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 ff6de4430a28..000000000000 --- a/libs/hbb_common/src/config.rs +++ /dev/null @@ -1,2541 +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 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"; -const ENCRYPT_MAX_LEN: usize = 128; - -#[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 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 BUILDIN_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)] - 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, - // 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(), - } - } -} - -#[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"); - if let Some(mut socks) = config.socks { - let (password, _, store) = - decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); - socks.password = password; - config.socks = Some(socks); - 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::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 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(); - } - - 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() -> 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" -); - -#[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() - } - - 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 { - 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(); - } -} - -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_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_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_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"; - - // 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"; - - // 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, - ]; - // 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, - ]; - // 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_VIDEO_SAVE_DIRECTORY, - OPTION_ENABLE_ABR, - OPTION_ALLOW_REMOVE_WALLPAPER, - OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, - OPTION_ALLOW_LINUX_HEADLESS, - OPTION_ENABLE_HWCODEC, - OPTION_APPROVE_MODE, - 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, - ]; - - // 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, - ]; -} - -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 235cb4837b16..000000000000 --- a/libs/hbb_common/src/fs.rs +++ /dev/null @@ -1,900 +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) -} - -#[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 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 10979f520e94..000000000000 --- 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 15ef31022c42..000000000000 --- a/libs/hbb_common/src/lib.rs +++ /dev/null @@ -1,499 +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 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/password_security.rs b/libs/hbb_common/src/password_security.rs deleted file mode 100644 index 49a2d4d9498e..000000000000 --- 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.bytes().len() > max_len { - return String::default(); - } - if version == "00" { - if let Ok(s) = encrypt(s.as_bytes(), max_len) { - 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, max_len) { - 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], max_len: usize) -> Result { - if !v.is_empty() && v.len() <= max_len { - 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 5e03b6816e4c..000000000000 --- a/libs/hbb_common/src/platform/linux.rs +++ /dev/null @@ -1,288 +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 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_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 dd83a87385b9..000000000000 --- 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 5dc004a81b7f..000000000000 --- 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 7481631ace17..000000000000 --- 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 57d9b68fe341..000000000000 --- 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 34d2c5109f5a..000000000000 --- 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 4cb0bf204b52..000000000000 --- 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 17f360ff90cb..000000000000 --- 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 68abd42df9a3..000000000000 --- 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 e39212bf28ba..5553c718811f 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.7" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index ced5baf327ff..9effbc5893b7 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 7b68d821c816..87d4897c2d50 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/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 64805e0e5082..529010f16072 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -21,7 +21,7 @@ cfg-if = "1.0" num_cpus = "1.15" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } -webm = { git = "https://github.com/21pages/rust-webm" } +webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} [dependencies.winapi] @@ -59,7 +59,6 @@ gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } gstreamer-video = { version = "0.16", optional = true } [dependencies.hwcodec] -git = "https://github.com/21pages/hwcodec" +git = "https://github.com/rustdesk-org/hwcodec" optional = true - diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 55a688633817..0c0cd274ea7b 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -55,7 +55,11 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { target = target.replace("x64", "x86"); } println!("cargo:info={}", target); - path.push("installed"); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } path.push(target); println!( "{}", diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs index 803a4343ce26..a867a2d3fcc6 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 6d68a7352f96..ca620608adbe 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/android/ffi.rs b/libs/scrap/src/android/ffi.rs index d77dcce98964..7433e6b090f9 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -5,16 +5,20 @@ use jni::sys::jboolean; use jni::JNIEnv; use jni::{ objects::{GlobalRef, JClass, JObject}, + strings::JNIString, JavaVM, }; +use hbb_common::{message_proto::MultiClipboards, protobuf::Message}; use jni::errors::{Error as JniError, Result as JniResult}; use lazy_static::lazy_static; use serde::Deserialize; use std::ops::Not; +use std::os::raw::c_void; use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; use std::sync::{Mutex, RwLock}; use std::time::{Duration, Instant}; + lazy_static! { static ref JVM: RwLock> = RwLock::new(None); static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info @@ -22,6 +26,9 @@ lazy_static! { static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); static ref NDK_CONTEXT_INITED: Mutex = Default::default(); static ref MEDIA_CODEC_INFOS: RwLock> = RwLock::new(None); + static ref CLIPBOARD_MANAGER: RwLock> = RwLock::new(None); + static ref CLIPBOARDS_HOST: Mutex> = Mutex::new(None); + static ref CLIPBOARDS_CLIENT: Mutex> = Mutex::new(None); } const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100); @@ -104,6 +111,14 @@ pub fn get_audio_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { AUDIO_RAW.lock().ok()?.take(dst, last) } +pub fn get_clipboards(client: bool) -> Option { + if client { + CLIPBOARDS_CLIENT.lock().ok()?.take() + } else { + CLIPBOARDS_HOST.lock().ok()?.take() + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate( env: JNIEnv, @@ -132,6 +147,27 @@ pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate( } } +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onClipboardUpdate( + env: JNIEnv, + _class: JClass, + buffer: JByteBuffer, +) { + if let Ok(data) = env.get_direct_buffer_address(&buffer) { + if let Ok(len) = env.get_direct_buffer_capacity(&buffer) { + let data = unsafe { std::slice::from_raw_parts(data, len) }; + if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) { + let is_client = data[0] == 1; + if is_client { + *CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips); + } else { + *CLIPBOARDS_HOST.lock().unwrap() = Some(clips); + } + } + } + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( env: JNIEnv, @@ -155,10 +191,36 @@ pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) { log::debug!("MainService init from java"); if let Ok(jvm) = env.get_java_vm() { - *JVM.write().unwrap() = Some(jvm); + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); if let Ok(context) = env.new_global_ref(ctx) { + let context_jobject = context.as_obj().as_raw() as *mut c_void; *MAIN_SERVICE_CTX.write().unwrap() = Some(context); - init_ndk_context().ok(); + init_ndk_context(java_vm, context_jobject); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setClipboardManager( + env: JNIEnv, + _class: JClass, + clipboard_manager: JObject, +) { + log::debug!("ClipboardManager init from java"); + if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); + if let Ok(manager) = env.new_global_ref(clipboard_manager) { + *CLIPBOARD_MANAGER.write().unwrap() = Some(manager); } } } @@ -272,6 +334,51 @@ pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> { } } +fn _call_clipboard_manager(name: S, sig: T, args: &[JValue]) -> JniResult<()> +where + S: Into, + T: Into + AsRef, +{ + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + env.call_method(cm, name, sig, args)?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> { + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + let data = env.byte_array_from_slice(data)?; + + env.call_method( + cm, + "rustUpdateClipboard", + "([B)V", + &[JValue::Object(&JObject::from(data))], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableClientClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + pub fn call_main_service_get_by_name(name: &str) -> JniResult { if let (Some(jvm), Some(ctx)) = ( JVM.read().unwrap().as_ref(), @@ -332,7 +439,14 @@ pub fn call_main_service_set_by_name( } } -fn init_ndk_context() -> JniResult<()> { +// Difference between MainService, MainActivity, JNI_OnLoad: +// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null. +// cpal: all three works +// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions +// On app start: JNI_OnLoad or MainActivity init context +// On service start first time: MainService replace the context + +fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) { let mut lock = NDK_CONTEXT_INITED.lock().unwrap(); if *lock { unsafe { @@ -340,18 +454,20 @@ fn init_ndk_context() -> JniResult<()> { } *lock = false; } - if let (Some(jvm), Some(ctx)) = ( - JVM.read().unwrap().as_ref(), - MAIN_SERVICE_CTX.read().unwrap().as_ref(), - ) { - unsafe { - ndk_context::initialize_android_context( - jvm.get_java_vm_pointer() as _, - ctx.as_obj().as_raw() as _, - ); - } - *lock = true; - return Ok(()); + unsafe { + ndk_context::initialize_android_context(java_vm, context_jobject); + #[cfg(feature = "hwcodec")] + hwcodec::android::ffmpeg_set_java_vm(java_vm); + } + *lock = true; +} + +// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init +#[no_mangle] +pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint { + if let Ok(env) = vm.get_env() { + let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void; + init_ndk_context(vm, res); } - Err(JniError::ThrowFailed(-1)) + jni::JNIVersion::V6.into() } diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index 00d6fe506813..4bf17a2fee3a 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. - const kRtpTicksPerSecond: i32 = 90000; - 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 + pub(super) const kTimeBaseDen: i64 = 1000; // Only positive speeds, range for real-time coding currently is: 6 - 8. // Lower means slower/better quality, higher means fastest/lower quality. @@ -108,7 +102,7 @@ mod webrtc { c.g_h = cfg.height; c.g_threads = codec_thread_num(64) as _; c.g_timebase.num = 1; - c.g_timebase.den = kRtpTicksPerSecond; + c.g_timebase.den = kTimeBaseDen as _; c.g_input_bit_depth = kBitDepth; if let Some(keyframe_interval) = cfg.keyframe_interval { c.kf_min_dist = 0; @@ -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 } @@ -313,7 +287,7 @@ impl EncoderApi for AomEncoder { } impl AomEncoder { - pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result { + pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result { let bpp = if self.i444 { 24 } else { 12 }; if data.len() < self.width * self.height * bpp / 8 { return Err(Error::FailedCall("len not enough".to_string())); @@ -333,13 +307,14 @@ impl AomEncoder { stride_align as _, data.as_ptr() as _, )); - + let pts = webrtc::kTimeBaseDen / 1000 * ms; + let duration = webrtc::kTimeBaseDen / 1000; call_aom!(aom_codec_encode( &mut self.ctx, &image, pts as _, - 1, // Duration - 0, // Flags + duration as _, // Duration + 0, // Flags )); Ok(EncodeFrames { @@ -369,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/codec.rs b/libs/scrap/src/common/codec.rs index 07ff0f91d243..662ac02a5423 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -1,8 +1,8 @@ use std::{ collections::HashMap, - ffi::c_void, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, + time::Instant, }; #[cfg(feature = "hwcodec")] @@ -15,7 +15,7 @@ use crate::{ aom::{self, AomDecoder, AomEncoder, AomEncoderConfig}, common::GoogleImage, vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId}, - CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, + CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture, }; use hbb_common::{ @@ -28,7 +28,6 @@ use hbb_common::{ SupportedDecoding, SupportedEncoding, VideoFrame, }, sysinfo::System, - tokio::time::Instant, ResultType, }; @@ -63,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; @@ -264,15 +261,20 @@ impl Encoder { .unwrap_or((PreferCodec::Auto.into(), 0)); let preference = most_frequent.enum_value_or(PreferCodec::Auto); - // auto: h265 > h264 > vp9/vp8 - let mut auto_codec = CodecFormat::VP9; + // auto: h265 > h264 > av1/vp9/vp8 + let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N"; + let mut auto_codec = if av1_useable && av1_test { + CodecFormat::AV1 + } else { + CodecFormat::VP9 + }; if h264_useable { auto_codec = CodecFormat::H264; } if h265_useable { auto_codec = CodecFormat::H265; } - if auto_codec == CodecFormat::VP9 { + if auto_codec == CodecFormat::VP9 || auto_codec == CodecFormat::AV1 { let mut system = System::new(); system.refresh_memory(); if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 { @@ -623,7 +625,7 @@ impl Decoder { &mut self, frame: &video_frame::Union, rgb: &mut ImageRgb, - _texture: &mut *mut c_void, + _texture: &mut ImageTexture, _pixelbuffer: &mut bool, chroma: &mut Option, ) -> ResultType { @@ -777,12 +779,16 @@ impl Decoder { fn handle_vram_video_frame( decoder: &mut VRamDecoder, frames: &EncodedVideoFrames, - texture: &mut *mut c_void, + texture: &mut ImageTexture, ) -> ResultType { let mut ret = false; for h26x in frames.frames.iter() { for image in decoder.decode(&h26x.data)? { - *texture = image.frame.texture; + *texture = ImageTexture { + texture: image.frame.texture, + w: image.frame.width as _, + h: image.frame.height as _, + }; ret = true; } } @@ -874,12 +880,16 @@ pub fn enable_directx_capture() -> bool { ) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +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 { @@ -895,22 +905,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 { @@ -978,3 +1025,123 @@ fn disable_av1() -> bool { // disable it for all 32 bit platforms std::mem::size_of::() == 4 } + +#[cfg(not(target_os = "ios"))] +pub fn test_av1() { + use hbb_common::config::keys::OPTION_AV1_TEST; + use hbb_common::rand::Rng; + use std::{sync::Once, time::Duration}; + + if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() { + log::info!("skip test av1"); + return; + } + + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let f = || { + 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; + let generate_fake_data = + |frame_index: u32, dst_fmt: EncodeYuvFormat| -> ResultType> { + let mut rng = hbb_common::rand::thread_rng(); + let mut bgra = vec![0u8; (width * height * 4) as usize]; + let gradient = frame_index as f32 / frame_count as f32; + // floating block + let x0 = (frame_index * move_step) % (width - block_size); + let y0 = (frame_index * move_step) % (height - block_size); + // Fill the block with random colors + for y in 0..block_size { + for x in 0..block_size { + let index = (((y0 + y) * width + x0 + x) * 4) as usize; + if index + 3 < bgra.len() { + let noise = rng.gen_range(0..255) as f32 / 255.0; + let value = (255.0 * gradient + noise * 50.0) as u8; + bgra[index] = value; + bgra[index + 1] = value; + bgra[index + 2] = value; + bgra[index + 3] = 255; + } + } + } + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_uv = dst_fmt.stride[1]; + let mut dst = vec![0u8; (dst_fmt.h * dst_stride_y * 2) as usize]; + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[dst_fmt.u..].as_mut_ptr(); + let dst_v = dst[dst_fmt.v..].as_mut_ptr(); + let res = unsafe { + crate::ARGBToI420( + bgra.as_ptr(), + (width * 4) as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ) + }; + if res != 0 { + bail!("ARGBToI420 failed: {}", res); + } + Ok(dst) + }; + let Ok(mut av1) = AomEncoder::new( + EncoderCfg::AOM(AomEncoderConfig { + width, + height, + quality, + keyframe_interval, + }), + i444, + ) else { + return false; + }; + let mut key_frame_time = Duration::ZERO; + let mut non_key_frame_time_sum = Duration::ZERO; + let pts = Instant::now(); + let yuvfmt = av1.yuvfmt(); + for i in 0..frame_count { + let Ok(yuv) = generate_fake_data(i, yuvfmt.clone()) else { + return false; + }; + let start = Instant::now(); + if av1 + .encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN) + .is_err() + { + log::debug!("av1 encode failed"); + if i == 0 { + return false; + } + } + if i == 0 { + key_frame_time = start.elapsed(); + } else { + non_key_frame_time_sum += start.elapsed(); + } + } + let non_key_frame_time = non_key_frame_time_sum / (frame_count - 1); + log::info!( + "av1 time: key: {:?}, non-key: {:?}, consume: {:?}", + key_frame_time, + non_key_frame_time, + pts.elapsed() + ); + key_frame_time < Duration::from_millis(90) + && non_key_frame_time < Duration::from_millis(30) + }; + std::thread::spawn(move || { + let v = f(); + Config::set_option( + OPTION_AV1_TEST.to_string(), + if v { "Y" } else { "N" }.to_string(), + ); + }); + }); +} diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4e653215eb6e..7ee9b3d61d57 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, }; @@ -15,7 +13,7 @@ use hbb_common::{ }; use hwcodec::{ common::{ - DataFormat, + DataFormat, HwcodecErrno, Quality::{self, *}, RateControl::{self, *}, }, @@ -31,6 +29,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12; pub const DEFAULT_FPS: i32 = 30; const DEFAULT_GOP: i32 = i32::MAX; const DEFAULT_HW_QUALITY: Quality = Quality_Default; +pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32; crate::generate_call_macro!(call_yuv, false); @@ -46,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, } @@ -66,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 base_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 { @@ -175,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, self.bitrate); + 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(()) } @@ -191,16 +190,8 @@ impl EncoderApi for HwRamEncoder { self.bitrate } - fn support_abr(&self) -> bool { - ["qsv", "vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) - } - fn support_changing_quality(&self) -> bool { - ["vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) + ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn latency_free(&self) -> bool { @@ -257,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 { @@ -498,6 +503,15 @@ pub struct HwCodecConfig { pub vram_decode: Vec, } +// HwCodecConfig2 is used to store the config in json format, +// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version. +// struct T { a: Vec, b: Vec} will fail if b is empty, but struct T { a: Vec, b: Vec} is ok. +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +struct HwCodecConfig2 { + #[serde(default)] + pub config: String, +} + // ipc server process start check process once, other process get from ipc server once // install: --server start check process, check process send to --server, ui get from --server // portable: ui start check process, check process send to ui @@ -509,7 +523,12 @@ impl HwCodecConfig { log::info!("set hwcodec config"); log::debug!("{config:?}"); #[cfg(any(windows, target_os = "macos"))] - hbb_common::config::common_store(&config, "_hwcodec"); + hbb_common::config::common_store( + &HwCodecConfig2 { + config: serde_json::to_string_pretty(&config).unwrap_or_default(), + }, + "_hwcodec", + ); *CONFIG.lock().unwrap() = Some(config); *CONFIG_SET_BY_IPC.lock().unwrap() = true; } @@ -587,7 +606,8 @@ impl HwCodecConfig { Some(c) => c, None => { log::info!("try load cached hwcodec config"); - let c = hbb_common::config::common_load::("_hwcodec"); + let c = hbb_common::config::common_load::("_hwcodec"); + let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default(); let new_signature = hwcodec::common::get_gpu_signature(); if c.signature == new_signature { log::debug!("load cached hwcodec config: {c:?}"); @@ -680,8 +700,8 @@ pub fn check_available_hwcodec() -> String { #[cfg(not(feature = "vram"))] let vram_string = "".to_owned(); let c = HwCodecConfig { - ram_encode: Encoder::available_encoders(ctx, Some(vram_string.clone())), - ram_decode: Decoder::available_decoders(Some(vram_string)), + ram_encode: Encoder::available_encoders(ctx, Some(vram_string)), + ram_decode: Decoder::available_decoders(), #[cfg(feature = "vram")] vram_encode: vram.0, #[cfg(feature = "vram")] diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 164f7157de30..cef718cc109d 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::*; @@ -96,6 +96,22 @@ impl ImageRgb { } } +pub struct ImageTexture { + pub texture: *mut c_void, + pub w: usize, + pub h: usize, +} + +impl Default for ImageTexture { + fn default() -> Self { + Self { + texture: std::ptr::null_mut(), + w: 0, + h: 0, + } + } +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? @@ -156,7 +172,7 @@ pub trait TraitPixelBuffer { #[cfg(not(any(target_os = "ios")))] pub enum Frame<'a> { PixelBuffer(PixelBuffer<'a>), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } #[cfg(not(any(target_os = "ios")))] @@ -164,7 +180,7 @@ impl Frame<'_> { pub fn valid<'a>(&'a self) -> bool { match self { Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(), - Frame::Texture(texture) => !texture.is_null(), + Frame::Texture((texture, _)) => !texture.is_null(), } } @@ -173,7 +189,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)?; @@ -186,7 +202,7 @@ impl Frame<'_> { pub enum EncodeInput<'a> { YUV(&'a [u8]), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } impl<'a> EncodeInput<'a> { @@ -197,7 +213,7 @@ impl<'a> EncodeInput<'a> { } } - pub fn texture(&self) -> ResultType<*mut c_void> { + pub fn texture(&self) -> ResultType<(*mut c_void, usize)> { match self { Self::Texture(f) => Ok(*f), _ => bail!("not texture frame"), @@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat { } } +impl From<&video_frame::Union> for CodecFormat { + fn from(it: &video_frame::Union) -> Self { + match it { + video_frame::Union::Vp8s(_) => CodecFormat::VP8, + video_frame::Union::Vp9s(_) => CodecFormat::VP9, + video_frame::Union::Av1s(_) => CodecFormat::AV1, + video_frame::Union::H264s(_) => CodecFormat::H264, + video_frame::Union::H265s(_) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + impl From<&CodecName> for CodecFormat { fn from(value: &CodecName) -> Self { match value { @@ -316,7 +345,7 @@ impl ToString for CodecFormat { CodecFormat::AV1 => "AV1".into(), CodecFormat::H264 => "H264".into(), CodecFormat::H265 => "H265".into(), - CodecFormat::Unknown => "Unknow".into(), + CodecFormat::Unknown => "Unknown".into(), } } } diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 52973c2b244b..6a1a6d60fcb9 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,22 +25,28 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, + pub display: usize, + pub tx: Option>, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext2 { pub filename: String, pub width: usize, pub height: usize, pub format: CodecFormat, - pub tx: Option>, } -impl RecorderContext { - pub fn set_filename(&mut self) -> ResultType<()> { - if !PathBuf::from(&self.dir).exists() { - std::fs::create_dir_all(&self.dir)?; +impl RecorderContext2 { + pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> { + if !PathBuf::from(&ctx.dir).exists() { + std::fs::create_dir_all(&ctx.dir)?; } - let file = if self.server { "incoming" } else { "outgoing" }.to_string() + let file = if ctx.server { "incoming" } else { "outgoing" }.to_string() + "_" - + &self.id.clone() + + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + + &format!("display{}_", ctx.display) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 @@ -50,11 +56,10 @@ impl RecorderContext { } else { ".mp4" }; - self.filename = PathBuf::from(&self.dir) + self.filename = PathBuf::from(&ctx.dir) .join(file) .to_string_lossy() .to_string(); - log::info!("video will save to {}", self.filename); Ok(()) } } @@ -63,7 +68,7 @@ unsafe impl Send for Recorder {} unsafe impl Sync for Recorder {} pub trait RecorderApi { - fn new(ctx: RecorderContext) -> ResultType + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType where Self: Sized; fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; @@ -78,13 +83,15 @@ pub enum RecordState { } pub struct Recorder { - pub inner: Box, + pub inner: Option>, ctx: RecorderContext, + ctx2: Option, pts: Option, + check_failed: bool, } impl Deref for Recorder { - type Target = Box; + type Target = Option>; fn deref(&self) -> &Self::Target { &self.inner @@ -98,114 +105,124 @@ impl DerefMut for Recorder { } impl Recorder { - pub fn new(mut ctx: RecorderContext) -> ResultType { - ctx.set_filename()?; - let recorder = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder { - inner: Box::new(WebmRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(feature = "hwcodec")] - _ => Recorder { - inner: Box::new(HwRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), - }; - recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone())); - Ok(recorder) + pub fn new(ctx: RecorderContext) -> ResultType { + Ok(Self { + inner: None, + ctx, + ctx2: None, + pts: None, + check_failed: false, + }) } - fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { - ctx.set_filename()?; - self.inner = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => { - Box::new(WebmRecorder::new(ctx.clone())?) + fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { + match self.ctx2 { + Some(ref ctx2) => { + if ctx2.width != w || ctx2.height != h || ctx2.format != format { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } } - #[cfg(feature = "hwcodec")] - _ => Box::new(HwRecorder::new(ctx.clone())?), - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), + None => { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } + } + let Some(ctx2) = &self.ctx2 else { + bail!("ctx2 is None"); }; - self.ctx = ctx; - self.pts = None; - self.send_state(RecordState::NewFile(self.ctx.filename.clone())); + if self.inner.is_none() { + self.inner = match format { + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new( + WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?, + )), + #[cfg(feature = "hwcodec")] + _ => Some(Box::new(HwRecorder::new( + self.ctx.clone(), + (*ctx2).clone(), + )?)), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + // pts is None when new inner is created + self.pts = None; + self.send_state(RecordState::NewFile(ctx2.filename.clone())); + } Ok(()) } - pub fn write_message(&mut self, msg: &Message) { + pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) { if let Some(message::Union::VideoFrame(vf)) = &msg.union { if let Some(frame) = &vf.union { - self.write_frame(frame).ok(); + self.write_frame(frame, w, h).ok(); } } } - pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { + pub fn write_frame( + &mut self, + frame: &video_frame::Union, + w: usize, + h: usize, + ) -> ResultType<()> { + if self.check_failed { + bail!("check failed"); + } + let format = CodecFormat::from(frame); + if format == CodecFormat::Unknown { + bail!("unsupported frame type"); + } + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } match frame { video_frame::Union::Vp8s(vp8s) => { - if self.ctx.format != CodecFormat::VP8 { - self.change(RecorderContext { - format: CodecFormat::VP8, - ..self.ctx.clone() - })?; - } for f in vp8s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Vp9s(vp9s) => { - if self.ctx.format != CodecFormat::VP9 { - self.change(RecorderContext { - format: CodecFormat::VP9, - ..self.ctx.clone() - })?; - } for f in vp9s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Av1s(av1s) => { - if self.ctx.format != CodecFormat::AV1 { - self.change(RecorderContext { - format: CodecFormat::AV1, - ..self.ctx.clone() - })?; - } for f in av1s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { - if self.ctx.format != CodecFormat::H264 { - self.change(RecorderContext { - format: CodecFormat::H264, - ..self.ctx.clone() - })?; - } for f in h264s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { - if self.ctx.format != CodecFormat::H265 { - self.change(RecorderContext { - format: CodecFormat::H265, - ..self.ctx.clone() - })?; - } for f in h265s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } _ => bail!("unsupported frame type"), @@ -214,13 +231,31 @@ impl Recorder { Ok(()) } - fn check_pts(&mut self, pts: i64) -> ResultType<()> { + fn check_pts( + &mut self, + pts: i64, + key: bool, + w: usize, + h: usize, + format: CodecFormat, + ) -> ResultType<()> { // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c + if self.pts.is_none() && !key { + bail!("first frame is not key frame"); + } let old_pts = self.pts; self.pts = Some(pts); if old_pts.clone().unwrap_or_default() > pts { log::info!("pts {:?} -> {}, change record filename", old_pts, pts); - self.change(self.ctx.clone())?; + self.inner = None; + self.ctx2 = None; + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } + self.pts = Some(pts); } Ok(()) } @@ -234,21 +269,22 @@ struct WebmRecorder { vt: VideoTrack, webm: Option>>, ctx: RecorderContext, + ctx2: RecorderContext2, key: bool, written: bool, start: Instant, } impl RecorderApi for WebmRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let out = match { OpenOptions::new() .write(true) .create_new(true) - .open(&ctx.filename) + .open(&ctx2.filename) } { Ok(file) => file, - Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?, Err(e) => return Err(e.into()), }; let mut webm = match mux::Segment::new(mux::Writer::new(out)) { @@ -256,18 +292,18 @@ impl RecorderApi for WebmRecorder { None => bail!("Failed to create webm mux"), }; let vt = webm.add_video_track( - ctx.width as _, - ctx.height as _, + ctx2.width as _, + ctx2.height as _, None, - if ctx.format == CodecFormat::VP9 { + if ctx2.format == CodecFormat::VP9 { mux::VideoCodecId::VP9 - } else if ctx.format == CodecFormat::VP8 { + } else if ctx2.format == CodecFormat::VP8 { mux::VideoCodecId::VP8 } else { mux::VideoCodecId::AV1 }, ); - if ctx.format == CodecFormat::AV1 { + if ctx2.format == CodecFormat::AV1 { // [129, 8, 12, 0] in 3.6.0, but zero works let codec_private = vec![0, 0, 0, 0]; if !webm.set_codec_private(vt.track_number(), &codec_private) { @@ -278,6 +314,7 @@ impl RecorderApi for WebmRecorder { vt, webm: Some(webm), ctx, + ctx2, key: false, written: false, start: Instant::now(), @@ -307,7 +344,7 @@ impl Drop for WebmRecorder { let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); @@ -316,8 +353,9 @@ impl Drop for WebmRecorder { #[cfg(feature = "hwcodec")] struct HwRecorder { - muxer: Muxer, + muxer: Option, ctx: RecorderContext, + ctx2: RecorderContext2, written: bool, key: bool, start: Instant, @@ -325,18 +363,19 @@ struct HwRecorder { #[cfg(feature = "hwcodec")] impl RecorderApi for HwRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let muxer = Muxer::new(MuxContext { - filename: ctx.filename.clone(), - width: ctx.width, - height: ctx.height, - is265: ctx.format == CodecFormat::H265, + filename: ctx2.filename.clone(), + width: ctx2.width, + height: ctx2.height, + is265: ctx2.format == CodecFormat::H265, framerate: crate::hwcodec::DEFAULT_FPS as _, }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; Ok(HwRecorder { - muxer, + muxer: Some(muxer), ctx, + ctx2, written: false, key: false, start: Instant::now(), @@ -348,7 +387,11 @@ impl RecorderApi for HwRecorder { self.key = true; } if self.key { - let ok = self.muxer.write_video(&frame.data, frame.key).is_ok(); + let ok = self + .muxer + .as_mut() + .map(|m| m.write_video(&frame.data, frame.key).is_ok()) + .unwrap_or_default(); if ok { self.written = true; } @@ -362,10 +405,12 @@ impl RecorderApi for HwRecorder { #[cfg(feature = "hwcodec")] impl Drop for HwRecorder { fn drop(&mut self) { - self.muxer.write_tail().ok(); + self.muxer.as_mut().map(|m| m.write_tail().ok()); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + // The process cannot access the file because it is being used by another process + self.muxer = None; + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 11b497fb3bc1..244f38ed57b2 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 a2b4d348c46f..747dbbe76db8 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::{base_bitrate, 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}, @@ -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 base_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"))), } @@ -101,7 +99,12 @@ impl EncoderApi for VRamEncoder { frame: EncodeInput, ms: i64, ) -> ResultType { - let texture = frame.texture()?; + let (texture, rotation) = frame.texture()?; + if rotation != 0 { + // to-do: support rotation + // Both the encoder and display(w,h) information need to be changed. + bail!("rotation not supported"); + } let mut vf = VideoFrame::new(); let mut frames = Vec::new(); for frame in self @@ -167,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; @@ -182,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 } @@ -280,31 +283,8 @@ 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) { diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 923606a82776..cc36b3c2308a 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 abd1f5026999..9220fa605939 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -7,10 +7,11 @@ use winapi::{ shared::{ dxgi::*, dxgi1_2::*, + dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM, dxgitype::*, minwindef::{DWORD, FALSE, TRUE, UINT}, ntdef::LONG, - windef::HMONITOR, + windef::{HMONITOR, RECT}, winerror::*, // dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, }, @@ -57,6 +58,7 @@ pub struct Capturer { saved_raw_data: Vec, // for faster compare and copy output_texture: bool, adapter_desc1: DXGI_ADAPTER_DESC1, + rotate: Rotate, } impl Capturer { @@ -151,6 +153,7 @@ impl Capturer { (*duplication).GetDesc(&mut desc); } } + let rotate = Self::create_rotations(device.0, context.0, &display); Ok(Capturer { device, @@ -168,9 +171,143 @@ impl Capturer { saved_raw_data: Vec::new(), output_texture: false, adapter_desc1, + rotate, }) } + fn create_rotations( + device: *mut ID3D11Device, + context: *mut ID3D11DeviceContext, + display: &Display, + ) -> Rotate { + let mut video_context: *mut ID3D11VideoContext = ptr::null_mut(); + let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut(); + let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut(); + let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut(); + let processor_rotation = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90), + DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180), + DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270), + _ => None, + }; + if let Some(processor_rotation) = processor_rotation { + println!("create rotations"); + if !device.is_null() && !context.is_null() { + unsafe { + (*context).QueryInterface( + &IID_ID3D11VideoContext, + &mut video_context as *mut *mut _ as *mut *mut _, + ); + if !video_context.is_null() { + (*device).QueryInterface( + &IID_ID3D11VideoDevice, + &mut video_device as *mut *mut _ as *mut *mut _, + ); + if !video_device.is_null() { + let (input_width, input_height) = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => { + (display.height(), display.width()) + } + _ => (display.width(), display.height()), + }; + let (output_width, output_height) = (display.width(), display.height()); + let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { + InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + InputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + InputWidth: input_width as _, + InputHeight: input_height as _, + OutputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + OutputWidth: output_width as _, + OutputHeight: output_height as _, + Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }; + (*video_device).CreateVideoProcessorEnumerator( + &content_desc, + &mut video_processor_enum, + ); + if !video_processor_enum.is_null() { + let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed(); + if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps) + { + if caps.FeatureCaps + & D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION + != 0 + { + (*video_device).CreateVideoProcessor( + video_processor_enum, + 0, + &mut video_processor, + ); + if !video_processor.is_null() { + (*video_context).VideoProcessorSetStreamRotation( + video_processor, + 0, + TRUE, + processor_rotation, + ); + (*video_context) + .VideoProcessorSetStreamAutoProcessingMode( + video_processor, + 0, + FALSE, + ); + (*video_context).VideoProcessorSetStreamFrameFormat( + video_processor, + 0, + D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + ); + (*video_context).VideoProcessorSetStreamSourceRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: input_width as _, + bottom: input_height as _, + }, + ); + (*video_context).VideoProcessorSetStreamDestRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: output_width as _, + bottom: output_height as _, + }, + ); + } + } + } + } + } + } + } + } + } + + let video_context = ComPtr(video_context); + let video_device = ComPtr(video_device); + let video_processor_enum = ComPtr(video_processor_enum); + let video_processor = ComPtr(video_processor); + let rotated_texture = ComPtr(ptr::null_mut()); + Rotate { + video_context, + video_device, + video_processor_enum, + video_processor, + texture: (rotated_texture, false), + } + } + pub fn is_gdi(&self) -> bool { self.gdi_capturer.is_some() } @@ -328,7 +465,7 @@ impl Capturer { } } - fn get_texture(&mut self, timeout: UINT) -> io::Result<*mut c_void> { + fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> { unsafe { if self.duplication.0.is_null() { return Err(std::io::ErrorKind::AddrNotAvailable.into()); @@ -352,7 +489,86 @@ impl Capturer { ); let texture = ComPtr(texture); self.texture = texture; - Ok(self.texture.0 as *mut c_void) + + let mut final_texture = self.texture.0 as *mut c_void; + let mut rotation = match self.display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => 0, + }; + if rotation != 0 + && !self.texture.is_null() + && !self.rotate.video_context.is_null() + && !self.rotate.video_device.is_null() + && !self.rotate.video_processor_enum.is_null() + && !self.rotate.video_processor.is_null() + { + let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed(); + (*self.texture.0).GetDesc(&mut desc); + if rotation == 90 || rotation == 270 { + let tmp = desc.Width; + desc.Width = desc.Height; + desc.Height = tmp; + } + if !self.rotate.texture.1 { + self.rotate.texture.1 = true; + let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut(); + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + (*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture); + self.rotate.texture.0 = ComPtr(rotated_texture); + } + if !self.rotate.texture.0.is_null() + && desc.Width == self.width as u32 + && desc.Height == self.height as u32 + { + let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { + FourCC: 0, + ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, + Texture2D: D3D11_TEX2D_VPIV { + ArraySlice: 0, + MipSlice: 0, + }, + }; + let mut input_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorInputView( + self.texture.0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &input_view_desc, + &mut input_view, + ); + if !input_view.is_null() { + let input_view = ComPtr(input_view); + let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC = + mem::zeroed(); + output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + output_view_desc.u.Texture2D_mut().MipSlice = 0; + let mut output_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorOutputView( + self.rotate.texture.0 .0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &output_view_desc, + &mut output_view, + ); + if !output_view.is_null() { + let output_view = ComPtr(output_view); + let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed(); + stream_data.Enable = TRUE; + stream_data.pInputSurface = input_view.0; + (*self.rotate.video_context.0).VideoProcessorBlt( + self.rotate.video_processor.0, + output_view.0, + 0, + 1, + &stream_data, + ); + final_texture = self.rotate.texture.0 .0 as *mut c_void; + rotation = 0; + } + } + } + } + Ok((final_texture, rotation)) } } @@ -656,3 +872,11 @@ fn wrap_hresult(x: HRESULT) -> io::Result<()> { }) .into()) } + +struct Rotate { + video_context: ComPtr, + video_device: ComPtr, + video_processor_enum: ComPtr, + video_processor: ComPtr, + texture: (ComPtr, bool), +} diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index c7f2e62ac04a..2f1e2a852670 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta use lazy_static::lazy_static; lazy_static! { - pub static ref RDP_RESPONSE: Mutex> = Mutex::new(None); + pub static ref RDP_SESSION_INFO: Mutex> = Mutex::new(None); } #[inline] pub fn close_session() { - let _ = RDP_RESPONSE.lock().unwrap().take(); + let _ = RDP_SESSION_INFO.lock().unwrap().take(); } #[inline] pub fn is_rdp_session_hold() -> bool { - RDP_RESPONSE.lock().unwrap().is_some() + RDP_SESSION_INFO.lock().unwrap().is_some() } pub fn try_close_session() { - let mut rdp_res = RDP_RESPONSE.lock().unwrap(); + let mut rdp_info = RDP_SESSION_INFO.lock().unwrap(); let mut close = false; - if let Some(rdp_res) = &*rdp_res { + if let Some(rdp_info) = &*rdp_info { // If is server running and restore token is supported, there's no need to keep the session. - if is_server_running() && rdp_res.is_support_restore_token { + if is_server_running() && rdp_info.is_support_restore_token { close = true; } } if close { - *rdp_res = None; + *rdp_info = None; } } -pub struct RdpResponse { +pub struct RdpSessionInfo { pub conn: Arc, pub streams: Vec, pub fd: OwnedFd, pub session: dbus::Path<'static>, pub is_support_restore_token: bool, + pub resolution: Arc>>, } #[derive(Debug, Clone, Copy)] pub struct PwStreamInfo { @@ -69,6 +70,12 @@ pub struct PwStreamInfo { size: (usize, usize), } +impl PwStreamInfo { + pub fn get_size(&self) -> (usize, usize) { + self.size + } +} + #[derive(Debug)] pub struct DBusError(String); @@ -105,24 +112,31 @@ pub struct PipeWireCapturable { } impl PipeWireCapturable { - fn new(conn: Arc, fd: OwnedFd, stream: PwStreamInfo) -> Self { + fn new( + conn: Arc, + fd: OwnedFd, + resolution: Arc>>, + stream: PwStreamInfo, + ) -> Self { // alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling // https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244 - let res = get_res(Self { + let size = get_res(Self { dbus_conn: conn.clone(), fd: fd.clone(), path: stream.path, source_type: stream.source_type, position: stream.position, size: stream.size, - }); + }) + .unwrap_or(stream.size); + *resolution.lock().unwrap() = Some(size); Self { dbus_conn: conn, fd, path: stream.path, source_type: stream.source_type, position: stream.position, - size: res.unwrap_or(stream.size), + size, } } } @@ -593,7 +607,7 @@ pub fn request_remote_desktop() -> Result< } } Err(Box::new(DBusError( - "Failed to obtain screen capture.".into(), +"Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.".into() ))) } @@ -813,7 +827,7 @@ fn on_start_response( } pub fn get_capturables() -> Result, Box> { - let mut rdp_connection = match RDP_RESPONSE.lock() { + let mut rdp_connection = match RDP_SESSION_INFO.lock() { Ok(conn) => conn, Err(err) => return Err(Box::new(err)), }; @@ -822,28 +836,36 @@ pub fn get_capturables() -> Result, Box> { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?; let conn = Arc::new(conn); - let rdp_res = RdpResponse { + let rdp_info = RdpSessionInfo { conn, streams, fd, session, is_support_restore_token, + resolution: Arc::new(Mutex::new(None)), }; - *rdp_connection = Some(rdp_res); + *rdp_connection = Some(rdp_info); } - let rdp_res = match rdp_connection.as_ref() { + let rdp_info = match rdp_connection.as_ref() { Some(res) => res, None => { return Err(Box::new(DBusError("RDP response is None.".into()))); } }; - Ok(rdp_res + Ok(rdp_info .streams .clone() .into_iter() - .map(|s| PipeWireCapturable::new(rdp_res.conn.clone(), rdp_res.fd.clone(), s)) + .map(|s| { + PipeWireCapturable::new( + rdp_info.conn.clone(), + rdp_info.fd.clone(), + rdp_info.resolution.clone(), + s, + ) + }) .collect()) } diff --git a/libs/scrap/src/x11/server.rs b/libs/scrap/src/x11/server.rs index e2ffdc74b4f6..f9983f7cf2a2 100644 --- a/libs/scrap/src/x11/server.rs +++ b/libs/scrap/src/x11/server.rs @@ -1,3 +1,4 @@ +use hbb_common::libc; use std::ptr; use std::rc::Rc; @@ -99,11 +100,16 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> if reply.is_null() { // TODO: Should seperate SHM disabled from SHM not supported? return Err(Error::UnsupportedExtension); - } else if e.is_null() { - return Ok(()); } else { - // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? - return Err(Error::Generic); + // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229 + libc::free(reply as *mut _); + if e.is_null() { + return Ok(()); + } else { + libc::free(e as *mut _); + // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? + return Err(Error::Generic); + } } } diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index eeeccaaec8be..dad333ee5b9d 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -5,7 +5,7 @@ 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 diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index f68be3c7e36f..133ff11debd7 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 @@ -14,15 +14,10 @@ case $1 in rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true # workaround temp dev build between 1.1.9 and 1.2.0 - ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) - waylandSupportVersion=21 - if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] + serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] then - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] - then - systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true - fi + systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true fi rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true fi diff --git a/res/PKGBUILD b/res/PKGBUILD index 6f1b4e6801f1..f27fd6d638dc 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.2.7 +pkgver=1.3.7 pkgrel=0 epoch= pkgdesc="" @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libvdpau' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') makedepends=() checkdepends=() optdepends=() @@ -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/com.rustdesk.RustDesk.policy b/res/com.rustdesk.RustDesk.policy deleted file mode 100644 index 55f13629b7fa..000000000000 --- a/res/com.rustdesk.RustDesk.policy +++ /dev/null @@ -1,23 +0,0 @@ - - - - RustDesk - https://rustdesk.com/ - rustdesk - - Change RustDesk options - Authentication is required to change RustDesk options - 要更改RustDesk选项, 需要您先通过身份验证 - 要變更RustDesk選項, 需要您先通過身份驗證 - Authentifizierung zum Ändern der RustDesk-Optionen - /usr/share/rustdesk/files/polkit - true - - auth_admin - auth_admin - auth_admin - - - diff --git a/res/devices.py b/res/devices.py index 4259abd41623..f9bf27352921 100755 --- a/res/devices.py +++ b/res/devices.py @@ -46,7 +46,7 @@ def view( devices.append(device) continue last_online = datetime.strptime( - device["last_online"], "%Y-%m-%dT%H:%M:%S" + device["last_online"].split(".")[0], "%Y-%m-%dT%H:%M:%S" ) # assuming date is in this format if (datetime.utcnow() - last_online).days >= offline_days: devices.append(device) @@ -128,6 +128,8 @@ def main(): ) args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] devices = view( args.url, diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index 91ba7e03c238..afe06fdbb0b1 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,6 +31,11 @@ UINT __stdcall CustomActionHello( return WcaFinalize(er); } +// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail. +// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied. +// `msiexec /i package.msi /qn` +// +// So we need to delete the files separately in install folder. UINT __stdcall RemoveInstallFolder( __in MSIHANDLE hInstall) { @@ -41,6 +46,7 @@ UINT __stdcall RemoveInstallFolder( LPWSTR installFolder = NULL; LPWSTR pwz = NULL; LPWSTR pwzData = NULL; + WCHAR runtimeBroker[1024] = { 0, }; hr = WcaInitialize(hInstall, "RemoveInstallFolder"); ExitOnFailure(hr, "Failed to initialize"); @@ -52,21 +58,22 @@ UINT __stdcall RemoveInstallFolder( hr = WcaReadStringFromCaData(&pwz, &installFolder); ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder); + SHFILEOPSTRUCTW fileOp; ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT)); - fileOp.wFunc = FO_DELETE; - fileOp.pFrom = installFolder; + fileOp.pFrom = runtimeBroker; fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT; nResult = SHFileOperationW(&fileOp); if (nResult == 0) { - WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has been deleted.", installFolder); + WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker); } else { - WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", installFolder, nResult); + WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult); } LExit: diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs index c39951019223..de9edb7f38f3 100644 --- a/res/msi/Package/Components/Folders.wxs +++ b/res/msi/Package/Components/Folders.wxs @@ -3,8 +3,26 @@ + + + + + + + + + + + + + + + + - + diff --git a/res/msi/Package/Components/Regs.wxs b/res/msi/Package/Components/Regs.wxs index 23d4b6b8d5b4..33d587b1e5a8 100644 --- a/res/msi/Package/Components/Regs.wxs +++ b/res/msi/Package/Components/Regs.wxs @@ -5,16 +5,16 @@ - - + + - + - + @@ -22,7 +22,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index fa8c402e1f52..c17d60edb792 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -4,7 +4,7 @@ - + @@ -12,12 +12,12 @@ - - - - + + + + - + @@ -29,7 +29,7 @@ - + @@ -40,11 +40,11 @@ - + - - + + @@ -54,14 +54,14 @@ - + - + @@ -88,8 +88,8 @@ - - + + - + diff --git a/res/msi/Package/Fragments/AddRemoveProperties.wxs b/res/msi/Package/Fragments/AddRemoveProperties.wxs index f93852867a17..a7139fab2c90 100644 --- a/res/msi/Package/Fragments/AddRemoveProperties.wxs +++ b/res/msi/Package/Fragments/AddRemoveProperties.wxs @@ -25,6 +25,9 @@ + + + diff --git a/res/msi/Package/Fragments/ShortcutProperties.wxs b/res/msi/Package/Fragments/ShortcutProperties.wxs index e800262fb10f..95bd4aaa370c 100644 --- a/res/msi/Package/Fragments/ShortcutProperties.wxs +++ b/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -25,11 +25,25 @@ - - + + - - + + + + + + + + + + + + + + + + @@ -45,8 +59,8 @@ - - + + diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 517bde2af8c7..1bd3986ddbb9 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -49,4 +49,7 @@ 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 e86494e8517f..bdd8471cfc0c 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -18,11 +18,11 @@ - + - + @@ -40,14 +40,17 @@ - + - + + + + diff --git a/res/msi/Package/UI/AnotherApp.wxs b/res/msi/Package/UI/AnotherApp.wxs index ea46812f7767..168d1b10e06b 100644 --- a/res/msi/Package/UI/AnotherApp.wxs +++ b/res/msi/Package/UI/AnotherApp.wxs @@ -1,7 +1,7 @@ -

+ diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs new file mode 100644 index 000000000000..6e27e2b28268 --- /dev/null +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs new file mode 100644 index 000000000000..bf59d569cac7 --- /dev/null +++ b/res/msi/Package/UI/MyInstallDlg.wxs @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 02db5bde43ab..9a43e9da6a9d 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -64,6 +64,12 @@ def make_parser(): parser.add_argument( "-c", "--custom", action="store_true", help="Is custom client", default=False ) + parser.add_argument( + "--conn-type", + type=str, + default="", + help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing', + ) parser.add_argument( "--app-name", type=str, default="RustDesk", help="The app name." ) @@ -84,7 +90,7 @@ def make_parser(): def read_lines_and_start_index(file_path, tag_start, tag_end): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() index_start = -1 index_end = -1 @@ -174,11 +180,11 @@ def func(lines, index_start): def replace_app_name_in_langs(app_name): langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language") for file_path in langs_dir.glob("*.wxl"): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() for i, line in enumerate(lines): lines[i] = line.replace("RustDesk", app_name) - with open(file_path, "w") as f: + with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) @@ -295,7 +301,7 @@ def func(lines, index_start): f'{indent}\n' ) lines_new.append( - f'{indent}\n' + f'{indent}\n' ) lines_new.append( f'{indent}\n' @@ -308,7 +314,7 @@ def func(lines, index_start): f'{indent}\n' ) lines_new.append( - f'{indent}\n' + f'{indent}\n' ) lines_new.append( f'{indent}\n' @@ -385,6 +391,26 @@ def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): else: return gen_custom_ARPSYSTEMCOMPONENT_False(args) +def gen_conn_type(args): + def func(lines, index_start): + indent = g_indent_unit * 3 + + lines_new = [] + if args.conn_type != "": + lines_new.append( + f"""{indent}\n""" + ) + + for i, line in enumerate(lines_new): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Fragments/AddRemoveProperties.wxs", + "", + "", + func, + ) def gen_content_between_tags(filename, tag_start, tag_end, func): target_file = Path(sys.argv[0]).parent.joinpath(filename) @@ -394,7 +420,7 @@ def gen_content_between_tags(filename, tag_start, tag_end, func): func(lines, index_start) - with open(target_file, "w") as f: + with open(target_file, "w", encoding="utf-8") as f: f.writelines(lines) return True @@ -454,19 +480,19 @@ def update_license_file(app_name): if app_name == "RustDesk": return license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf") - with open(license_file, "r") as f: + with open(license_file, "r", encoding="utf-8") as f: license_content = f.read() license_content = license_content.replace("website rustdesk.com and other ", "") license_content = license_content.replace("RustDesk", app_name) license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE) - with open(license_file, "w") as f: + with open(license_file, "w", encoding="utf-8") as f: f.write(license_content) def replace_component_guids_in_wxs(): langs_dir = Path(sys.argv[0]).parent.joinpath("Package") for file_path in langs_dir.glob("**/*.wxs"): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() # @@ -475,7 +501,7 @@ def replace_component_guids_in_wxs(): if match: lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line) - with open(file_path, "w") as f: + with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) @@ -506,6 +532,9 @@ def replace_component_guids_in_wxs(): if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): sys.exit(-1) + if not gen_conn_type(args): + sys.exit(-1) + if not gen_auto_component(app_name, dist_dir): sys.exit(-1) diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index c4fe69e67788..d56838d3c3fd 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,11 +1,16 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +URL: https://rustdesk.com +Vendor: rustdesk +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +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. @@ -19,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" @@ -28,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 @@ -38,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 @@ -55,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 @@ -78,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 90e45af9cbf4..771c8a12e7ff 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,11 +1,16 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base +URL: https://rustdesk.com +Vendor: rustdesk +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-base +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. @@ -19,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" @@ -28,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 @@ -38,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 @@ -55,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 @@ -78,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 d84e14812387..79b26d6f07c9 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,10 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +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. @@ -18,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 @@ -32,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 @@ -42,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 a6d6a956a670..eb4a9a7ad381 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,9 +1,14 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base +URL: https://rustdesk.com +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. @@ -18,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 @@ -32,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 @@ -43,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/users.py b/res/users.py new file mode 100755 index 000000000000..54297f06ae82 --- /dev/null +++ b/res/users.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import requests +import argparse +from datetime import datetime, timedelta + + +def view( + url, + token, + name=None, + group_name=None, +): + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "name": name, + "group_name": group_name, + } + + params = { + k: "%" + v + "%" if (v != "-" and "%" not in v) else v + for k, v in params.items() + if v is not None + } + params["pageSize"] = pageSize + + users = [] + + current = 1 + + while True: + params["current"] = current + response = requests.get(f"{url}/api/users", headers=headers, params=params) + response_json = response.json() + + data = response_json.get("data", []) + users.extend(data) + + total = response_json.get("total", 0) + current += pageSize + if len(data) < pageSize or current > total: + break + + return users + + +def check(response): + if response.status_code == 200: + try: + response_json = response.json() + return response_json + except ValueError: + return response.text or "Success" + else: + return "Failed", response.status_code, response.text + + +def disable(url, token, guid, name): + print("Disable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/disable", headers=headers) + return check(response) + + +def enable(url, token, guid, name): + print("Enable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/enable", headers=headers) + return check(response) + + +def delete(url, token, guid, name): + print("Delete", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/users/{guid}", headers=headers) + return check(response) + + +def main(): + parser = argparse.ArgumentParser(description="User manager") + parser.add_argument( + "command", + choices=["view", "disable", "enable", "delete"], + help="Command to execute", + ) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument( + "--token", required=True, help="Bearer token for authentication" + ) + parser.add_argument("--name", help="User name") + parser.add_argument("--group_name", help="Group name") + + args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] + + users = view( + args.url, + args.token, + args.name, + args.group_name, + ) + + if args.command == "view": + for user in users: + print(user) + elif args.command == "disable": + for user in users: + response = disable(args.url, args.token, user["guid"], user["name"]) + print(response) + elif args.command == "enable": + for user in users: + response = enable(args.url, args.token, user["guid"], user["name"]) + print(response) + elif args.command == "delete": + for user in users: + response = delete(args.url, args.token, user["guid"], user["name"]) + print(response) + + +if __name__ == "__main__": + main() diff --git a/res/vcpkg/aom/portfile.cmake b/res/vcpkg/aom/portfile.cmake index 2df452a640e7..24b025173486 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 78ccc8989099..9ff755f6be6e 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 000000000000..ced7ba86be2b --- /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 000000000000..f1f6e72bee3f --- /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 9308e714a6bb..68b7503b2442 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 000000000000..c22f9c1999d0 --- /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 b2e5501a13a3..000000000000 --- 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 000000000000..f47e82ed8a20 --- /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 000000000000..dbce2f53b8e7 --- /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 000000000000..c2e1d8ff0d7b --- /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 000000000000..b22b40d1f377 --- /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 000000000000..6ff63c3718d6 --- /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 000000000000..bad42798c83e --- /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/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch b/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch deleted file mode 100644 index 475fb627f3e2..000000000000 --- a/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch +++ /dev/null @@ -1,95 +0,0 @@ -From afe89a70f6bc7ebd0a6a0a31101801b88cbd60ee Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sun, 5 May 2024 12:45:23 +0800 -Subject: [PATCH] use release/7.0's update_bitrate - -Signed-off-by: 21pages ---- - libavcodec/qsvenc.c | 39 +++++++++++++++++++++++++++++++++++++++ - libavcodec/qsvenc.h | 6 ++++++ - 2 files changed, 45 insertions(+) - -diff --git a/libavcodec/qsvenc.c b/libavcodec/qsvenc.c -index 2382c2f5f7..9b34f37eb3 100644 ---- a/libavcodec/qsvenc.c -+++ b/libavcodec/qsvenc.c -@@ -714,6 +714,11 @@ static int init_video_param(AVCodecContext *avctx, QSVEncContext *q) - brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), - initial_delay_in_kilobytes) + 0x10000) / 0x10000; - -+ q->old_rc_buffer_size = avctx->rc_buffer_size; -+ q->old_rc_initial_buffer_occupancy = avctx->rc_initial_buffer_occupancy; -+ q->old_bit_rate = avctx->bit_rate; -+ q->old_rc_max_rate = avctx->rc_max_rate; -+ - switch (q->param.mfx.RateControlMethod) { - case MFX_RATECONTROL_CBR: - case MFX_RATECONTROL_VBR: -@@ -1657,6 +1662,39 @@ static int update_qp(AVCodecContext *avctx, QSVEncContext *q, - return updated; - } - -+static int update_bitrate(AVCodecContext *avctx, QSVEncContext *q) -+{ -+ int updated = 0; -+ int target_bitrate_kbps, max_bitrate_kbps, brc_param_multiplier; -+ int buffer_size_in_kilobytes, initial_delay_in_kilobytes; -+ -+ UPDATE_PARAM(q->old_rc_buffer_size, avctx->rc_buffer_size); -+ UPDATE_PARAM(q->old_rc_initial_buffer_occupancy, avctx->rc_initial_buffer_occupancy); -+ UPDATE_PARAM(q->old_bit_rate, avctx->bit_rate); -+ UPDATE_PARAM(q->old_rc_max_rate, avctx->rc_max_rate); -+ if (!updated) -+ return 0; -+ -+ buffer_size_in_kilobytes = avctx->rc_buffer_size / 8000; -+ initial_delay_in_kilobytes = avctx->rc_initial_buffer_occupancy / 8000; -+ target_bitrate_kbps = avctx->bit_rate / 1000; -+ max_bitrate_kbps = avctx->rc_max_rate / 1000; -+ brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), -+ initial_delay_in_kilobytes) + 0x10000) / 0x10000; -+ -+ q->param.mfx.BufferSizeInKB = buffer_size_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.InitialDelayInKB = initial_delay_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.TargetKbps = target_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.MaxKbps = max_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.BRCParamMultiplier = brc_param_multiplier; -+ av_log(avctx, AV_LOG_VERBOSE, -+ "Reset BufferSizeInKB: %d; InitialDelayInKB: %d; " -+ "TargetKbps: %d; MaxKbps: %d; BRCParamMultiplier: %d\n", -+ q->param.mfx.BufferSizeInKB, q->param.mfx.InitialDelayInKB, -+ q->param.mfx.TargetKbps, q->param.mfx.MaxKbps, q->param.mfx.BRCParamMultiplier); -+ return updated; -+} -+ - static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - const AVFrame *frame) - { -@@ -1666,6 +1704,7 @@ static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - return 0; - - needReset = update_qp(avctx, q, frame); -+ needReset |= update_bitrate(avctx, q); - if (!needReset) - return 0; - -diff --git a/libavcodec/qsvenc.h b/libavcodec/qsvenc.h -index b754ac4b56..5745533165 100644 ---- a/libavcodec/qsvenc.h -+++ b/libavcodec/qsvenc.h -@@ -224,6 +224,12 @@ typedef struct QSVEncContext { - int min_qp_p; - int max_qp_b; - int min_qp_b; -+ -+ // These are used for bitrate control reset -+ int old_bit_rate; -+ int old_rc_buffer_size; -+ int old_rc_initial_buffer_occupancy; -+ int old_rc_max_rate; - } QSVEncContext; - - int ff_qsv_enc_init(AVCodecContext *avctx, QSVEncContext *q); --- -2.43.0.windows.1 - diff --git a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch b/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch deleted file mode 100644 index 49aef6947952..000000000000 --- a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch +++ /dev/null @@ -1,161 +0,0 @@ -From 8fd62e4ecd058b09abf8847be5fbbf0eef44a90f Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Tue, 16 Jul 2024 14:58:33 +0800 -Subject: [PATCH] amf colorspace - -Signed-off-by: 21pages ---- - libavcodec/amfenc.h | 1 + - libavcodec/amfenc_h264.c | 39 +++++++++++++++++++++++++++++++++ - libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 87 insertions(+) - -diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 31172645f2..493e01603d 100644 ---- a/libavcodec/amfenc.h -+++ b/libavcodec/amfenc.h -@@ -23,6 +23,7 @@ - - #include - #include -+#include - - #include "libavutil/fifo.h" - -diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index f55dbc80f0..5a6b6e164f 100644 ---- a/libavcodec/amfenc_h264.c -+++ b/libavcodec/amfenc_h264.c -@@ -139,6 +139,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); -@@ -199,11 +202,47 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) - 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) { - if (ctx->qp_i != -1 || ctx->qp_p != -1 || ctx->qp_b != -1) { -diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 7a40bcad31..0260f43c81 100644 ---- a/libavcodec/amfenc_hevc.c -+++ b/libavcodec/amfenc_hevc.c -@@ -106,6 +106,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); -@@ -130,6 +133,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - case FF_PROFILE_HEVC_MAIN: - profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; - break; -+ case FF_PROFILE_HEVC_MAIN_10: -+ profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; -+ break; - default: - break; - } -@@ -158,6 +164,47 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - 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/7.0/0001-android-mediacodec-encode-align-64.patch b/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch deleted file mode 100644 index d46c54af6da1..000000000000 --- a/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch +++ /dev/null @@ -1,40 +0,0 @@ -From be3d9d8092720bbe4239212648d2e9c4ffd7f40c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Wed, 22 May 2024 17:09:28 +0800 -Subject: [PATCH] android mediacodec encode align 64 - -Signed-off-by: 21pages ---- - libavcodec/mediacodecenc.c | 11 ++++++----- - 1 file changed, 6 insertions(+), 5 deletions(-) - -diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c -index 984014f1b1..8dcd3dcd64 100644 ---- a/libavcodec/mediacodecenc.c -+++ b/libavcodec/mediacodecenc.c -@@ -200,16 +200,17 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) - ff_AMediaFormat_setString(format, "mime", codec_mime); - // Workaround the alignment requirement of mediacodec. We can't do it - // silently for AV_PIX_FMT_MEDIACODEC. -+ const int align = 64; - if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC) { -- s->width = FFALIGN(avctx->width, 16); -- s->height = FFALIGN(avctx->height, 16); -+ s->width = FFALIGN(avctx->width, align); -+ s->height = FFALIGN(avctx->height, align); - } else { - s->width = avctx->width; - s->height = avctx->height; -- if (s->width % 16 || s->height % 16) -+ if (s->width % align || s->height % align) - av_log(avctx, AV_LOG_WARNING, -- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n", -- s->width, s->height); -+ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n", -+ s->width, s->height, align); - } - ff_AMediaFormat_setInt32(format, "width", s->width); - ff_AMediaFormat_setInt32(format, "height", s->height); --- -2.34.1 - diff --git a/res/vcpkg/ffmpeg/5.1/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 similarity index 62% rename from res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch rename to res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch index 245a470d39f2..4fbce0d48494 100644 --- a/res/vcpkg/ffmpeg/5.1/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,9 +1,9 @@ -From f0b694749b38b2cfd94df4eed10e667342c234e5 Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 15:33:24 +0800 -Subject: [PATCH 1/2] avcodec/amfenc: add query_timeout option for h264/hevc +From da6921d5bcb50961193526f47aa2dbe71ee5fe81 Mon Sep 17 00:00:00 2001 +From: 21pages +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 +Signed-off-by: 21pages --- libavcodec/amfenc.h | 1 + libavcodec/amfenc_h264.c | 4 ++++ @@ -11,10 +11,10 @@ Signed-off-by: 21pages 3 files changed, 9 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 1ab98d2f78..e92120ea39 100644 +index d985d01bb1..320c66919e 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -87,6 +87,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,18 +23,18 @@ index 1ab98d2f78..e92120ea39 100644 // Dynamic options, can be set after Init() call diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index efb04589f6..f55dbc80f0 100644 +index 8edd39c633..6ad4961b2f 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -121,6 +121,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 }, - { NULL } - }; -@@ -155,6 +156,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -228,6 +229,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); @@ -42,21 +42,21 @@ index efb04589f6..f55dbc80f0 100644 + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_H264_BASELINE: + case AV_PROFILE_H264_BASELINE: profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 8ab9330730..7a40bcad31 100644 +index 4898824f3a..22cb95c7ce 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -89,6 +89,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 }, - { NULL } - }; -@@ -122,6 +123,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -194,6 +195,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); @@ -64,7 +64,7 @@ index 8ab9330730..7a40bcad31 100644 + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_HEVC_MAIN: + case AV_PROFILE_HEVC_MAIN: profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; -- 2.43.0.windows.1 diff --git a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch similarity index 68% rename from res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch rename to res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch index 13b055ef2894..f2ec5df321e5 100644 --- a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch +++ b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -1,19 +1,19 @@ -From 4d0d20d96ad458cfec0444b9be0182ca6085ee0c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 16:02:44 +0800 -Subject: [PATCH 2/2] libavcodec/amfenc: reconfig when bitrate change +From 8d061adb7b00fc765b8001307c025437ef1cad88 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:32:16 +0800 +Subject: [PATCH 2/5] libavcodec/amfenc: reconfig when bitrate change -Signed-off-by: 21pages +Signed-off-by: 21pages --- libavcodec/amfenc.c | 20 ++++++++++++++++++++ libavcodec/amfenc.h | 1 + 2 files changed, 21 insertions(+) diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c -index a033e1220e..3eab01a903 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 a033e1220e..3eab01a903 100644 // configure AMF logger // the return of these functions indicates old state and do not affect behaviour -@@ -575,6 +576,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,9 +45,9 @@ index a033e1220e..3eab01a903 100644 int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) { AmfContext *ctx = avctx->priv_data; -@@ -586,6 +604,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) - AVFrame *frame = ctx->delayed_frame; - int block_and_wait; +@@ -653,6 +671,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + int query_output_data_flag = 0; + AMF_RESULT res_resubmit; + reconfig_encoder(avctx); + @@ -55,13 +55,13 @@ index a033e1220e..3eab01a903 100644 return AVERROR(EINVAL); diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index e92120ea39..31172645f2 100644 +index 320c66919e..481e0fb75d 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -107,6 +107,7 @@ typedef struct AmfContext { - int me_half_pel; - int me_quarter_pel; - int aud; +@@ -115,6 +115,7 @@ typedef struct AmfContext { + int max_b_frames; + int qvbr_quality_level; + int hw_high_motion_quality_boost; + int64_t av_bitrate; // HEVC - specific options diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch new file mode 100644 index 000000000000..77b41a7ada83 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -0,0 +1,85 @@ +From d74de94b49efcf7a0b25673ace6016938d1b9272 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 14:12:01 +0800 +Subject: [PATCH 3/5] videotoolbox changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/videotoolboxenc.c | 40 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) + +diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c +index da7b291b03..3c866177f5 100644 +--- a/libavcodec/videotoolboxenc.c ++++ b/libavcodec/videotoolboxenc.c +@@ -279,6 +279,8 @@ typedef struct VTEncContext { + int max_slice_bytes; + int power_efficient; + int max_ref_frames; ++ ++ int last_bit_rate; + } VTEncContext; + + 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]; + ++ vtctx->last_bit_rate = bit_rate; + int status = VTCompressionSessionCreate(kCFAllocatorDefault, + avctx->width, + avctx->height, +@@ -2638,6 +2641,42 @@ out: + return status; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ VTEncContext *vtctx = avctx->priv_data; ++ ++ if (avctx->codec_id != AV_CODEC_ID_PRORES) { ++ if (avctx->bit_rate != vtctx->last_bit_rate) { ++ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate); ++ vtctx->last_bit_rate = avctx->bit_rate; ++ SInt32 bit_rate = avctx->bit_rate; ++ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault, ++ kCFNumberSInt32Type, ++ &bit_rate); ++ if (!bit_rate_num) return; ++ ++ if (vtctx->constant_bit_rate) { ++ int status = VTSessionSetProperty(vtctx->session, ++ compat_keys.kVTCompressionPropertyKey_ConstantBitRate, ++ bit_rate_num); ++ if (status == kVTPropertyNotSupportedErr) { ++ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n"); ++ } ++ } else { ++ int status = VTSessionSetProperty(vtctx->session, ++ kVTCompressionPropertyKey_AverageBitRate, ++ bit_rate_num); ++ if (status) { ++ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); ++ } ++ } ++ ++ CFRelease(bit_rate_num); ++ } ++ } ++} ++ ++ + static av_cold int vtenc_frame( + AVCodecContext *avctx, + AVPacket *pkt, +@@ -2650,6 +2689,7 @@ static av_cold int vtenc_frame( + CMSampleBufferRef buf = NULL; + ExtraSEI sei = {0}; + ++ update_config(avctx); + if (frame) { + status = vtenc_send_frame(avctx, vtctx, frame); + +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch new file mode 100644 index 000000000000..4a552dda0fcd --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -0,0 +1,246 @@ +From 7323bd68c1b34e9298ea557ff7a3e1883b653957 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 14:28:16 +0800 +Subject: [PATCH 4/5] mediacodec changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/mediacodec_wrapper.c | 98 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 ++++++ + 3 files changed, 123 insertions(+) + +diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c +index 96c886666a..06b8504304 100644 +--- a/libavcodec/mediacodec_wrapper.c ++++ b/libavcodec/mediacodec_wrapper.c +@@ -35,6 +35,8 @@ + #include "ffjni.h" + #include "mediacodec_wrapper.h" + ++#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate" ++ + struct JNIAMediaCodecListFields { + + jclass mediacodec_list_class; +@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields { + jmethodID set_input_surface_id; + jmethodID signal_end_of_input_stream_id; + ++ jmethodID set_parameters_id; ++ + jclass mediainfo_class; + + jmethodID init_id; +@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = { + { "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 }, + { "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 }, + ++ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 }, ++ + { "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 }, + + { "android/media/MediaCodec.BufferInfo", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, +@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni { + + static const FFAMediaCodec media_codec_jni; + ++struct JNIABundleFields ++{ ++ jclass bundle_class; ++ jmethodID init_id; ++ jmethodID put_int_id; ++}; ++ ++#define OFFSET(x) offsetof(struct JNIABundleFields, x) ++static const struct FFJniField jni_abundle_mapping[] = { ++ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 }, ++ ++ { "android/os/Bundle", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, ++ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 }, ++ ++ { NULL } ++}; ++#undef OFFSET ++ + #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ + (env) = ff_jni_get_env(log_ctx); \ + if (!(env)) { \ +@@ -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; ++ struct JNIABundleFields jfields = { 0 }; ++ jobject object = NULL; ++ jstring key = NULL; ++ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx; ++ void *log_ctx = codec; ++ int ret = -1; ++ ++ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL); ++ ++ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n"); ++ goto fail; ++ } ++ ++ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id); ++ if (!object) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n"); ++ goto fail; ++ } ++ ++ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx); ++ if (!key) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ if (!codec->jfields.set_parameters_id) { ++ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ ret = 0; ++ ++fail: ++ if (key) { ++ (*env)->DeleteLocalRef(env, key); ++ } ++ if (object) { ++ (*env)->DeleteLocalRef(env, object); ++ } ++ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx); ++ ++ return ret; ++} ++ ++static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate); ++} ++ + static const FFAMediaFormat media_format_jni = { + .class = &amediaformat_class, + +@@ -1821,6 +1909,8 @@ static const FFAMediaCodec media_codec_jni = { + .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate, + }; + + typedef struct FFAMediaFormatNdk { +@@ -2335,6 +2425,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n"); ++ return -1; ++} ++ + static const FFAMediaFormat media_format_ndk = { + .class = &amediaformat_ndk_class, + +@@ -2396,6 +2492,8 @@ static const FFAMediaCodec media_codec_ndk = { + .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate, + }; + + FFAMediaFormat *ff_AMediaFormat_new(int ndk) +diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h +index 11a4260497..86c64556ad 100644 +--- a/libavcodec/mediacodec_wrapper.h ++++ b/libavcodec/mediacodec_wrapper.h +@@ -219,6 +219,8 @@ struct FFAMediaCodec { + + // For encoder with FFANativeWindow as input. + int (*signalEndOfInputStream)(FFAMediaCodec *); ++ ++ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate); + }; + + static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec) +@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec) + return codec->signalEndOfInputStream(codec); + } + ++static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate) ++{ ++ return codec->setDynamicBitrate(codec, bitrate); ++} ++ + int ff_Build_SDK_INT(AVCodecContext *avctx); + + enum FFAMediaFormatColorRange { +diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c +index 6ca3968a24..221f7360f4 100644 +--- a/libavcodec/mediacodecenc.c ++++ b/libavcodec/mediacodecenc.c +@@ -76,6 +76,8 @@ typedef struct MediaCodecEncContext { + int level; + int pts_as_dts; + int extract_extradata; ++ ++ int last_bit_rate; + } MediaCodecEncContext; + + enum { +@@ -193,6 +195,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) + int ret; + int gop; + ++ s->last_bit_rate = avctx->bit_rate; ++ + if (s->use_ndk_codec < 0) + s->use_ndk_codec = !av_jni_get_java_vm(avctx); + +@@ -542,11 +546,25 @@ static int mediacodec_send(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ MediaCodecEncContext *s = avctx->priv_data; ++ if (avctx->bit_rate != s->last_bit_rate) { ++ s->last_bit_rate = avctx->bit_rate; ++ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) { ++ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate); ++ } else { ++ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate); ++ } ++ } ++} ++ + static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt) + { + MediaCodecEncContext *s = avctx->priv_data; + int ret; + ++ update_config(avctx); + // Return on three case: + // 1. Serious error + // 2. Got a packet success +-- +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 new file mode 100644 index 000000000000..a62be5a81956 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch @@ -0,0 +1,1883 @@ +From 95ebc0ad912447ba83cacb197f506b881f82179e Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 15:29:21 +0800 +Subject: [PATCH 1/2] dlopen libva + +Signed-off-by: 21pages +--- + libavcodec/vaapi_decode.c | 96 ++++++----- + libavcodec/vaapi_encode.c | 173 ++++++++++--------- + libavcodec/vaapi_encode_h264.c | 3 +- + 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 a59194340f..e202b673f4 100644 +--- a/libavcodec/vaapi_decode.c ++++ b/libavcodec/vaapi_decode.c +@@ -38,17 +38,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, + size_t size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID buffer; + + av_assert0(pic->nb_param_buffers + 1 <= MAX_PARAM_BUFFERS); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + type, size, 1, (void*)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)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -69,6 +70,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + size_t slice_size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int index; + +@@ -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, nb_params, (void*)params_data, + &pic->slice_buffers[index]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " +- "parameter buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameter buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -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]); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VASliceDataBufferType, + slice_size, 1, (void*)slice_data, + &pic->slice_buffers[index + 1]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " + "data buffer (size %zu): %d (%s).\n", +- slice_size, vas, vaErrorStr(vas)); +- vaDestroyBuffer(ctx->hwctx->display, ++ slice_size, vas, vaf->vaErrorStr(vas)); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[index]); + return AVERROR(EIO); + } +@@ -127,26 +129,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int i; + + for (i = 0; i < pic->nb_param_buffers; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "parameter buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + + for (i = 0; i < 2 * pic->nb_slices; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy slice " + "slice buffer %#x: %d (%s).\n", +- pic->slice_buffers[i], vas, vaErrorStr(vas)); ++ pic->slice_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + } +@@ -155,6 +158,7 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + 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); + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->output_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload decode " +- "parameters: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameters: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->slice_buffers, 2 * pic->nb_slices); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload slices: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & + AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) +@@ -213,10 +217,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + goto exit; + + fail_with_picture: +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "after error: %d (%s).\n", vas, vaErrorStr(vas)); ++ "after error: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + fail: + ff_vaapi_decode_destroy_buffers(avctx, pic); +@@ -304,6 +308,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + AVHWFramesContext *frames) + { + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAStatus vas; + VASurfaceAttrib *attr; + enum AVPixelFormat source_format, best_format, format; +@@ -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); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + NULL, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOSYS); + } + +@@ -325,11 +330,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + if (!attr) + return AVERROR(ENOMEM); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + attr, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + av_freep(&attr); + return AVERROR(ENOSYS); + } +@@ -471,6 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + + AVHWDeviceContext *device = (AVHWDeviceContext*)device_ref->data; + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + + codec_desc = avcodec_descriptor_get(avctx->codec_id); + if (!codec_desc) { +@@ -478,7 +484,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- profile_count = vaMaxNumProfiles(hwctx->display); ++ profile_count = vaf->vaMaxNumProfiles(hwctx->display); + profile_list = av_malloc_array(profile_count, + sizeof(VAProfile)); + if (!profile_list) { +@@ -486,11 +492,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- vas = vaQueryConfigProfiles(hwctx->display, ++ vas = vaf->vaQueryConfigProfiles(hwctx->display, + profile_list, &profile_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -550,12 +556,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + } + } + +- vas = vaCreateConfig(hwctx->display, matched_va_profile, ++ vas = vaf->vaCreateConfig(hwctx->display, matched_va_profile, + VAEntrypointVLD, NULL, 0, + va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create decode " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -638,7 +644,7 @@ fail: + av_hwframe_constraints_free(&constraints); + av_freep(&hwconfig); + if (*va_config != VA_INVALID_ID) { +- vaDestroyConfig(hwctx->display, *va_config); ++ vaf->vaDestroyConfig(hwctx->display, *va_config); + *va_config = VA_INVALID_ID; + } + av_freep(&profile_list); +@@ -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; ++ VAAPIDynLoadFunctions *vaf; + VAConfigID va_config = VA_INVALID_ID; + int err; + + 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); +@@ -664,7 +672,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + return err; + + if (va_config != VA_INVALID_ID) +- vaDestroyConfig(hwctx->display, va_config); ++ vaf->vaDestroyConfig(hwctx->display, va_config); + + return 0; + } +@@ -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; ++ VAAPIDynLoadFunctions *vaf; + VAStatus vas; + int err; + +@@ -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) + goto fail; + +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + avctx->coded_width, avctx->coded_height, + VA_PROGRESSIVE, + ctx->hwfc->surface_ids, +@@ -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 " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -718,22 +732,28 @@ fail: + int ff_vaapi_decode_uninit(AVCodecContext *avctx) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + VAStatus vas; + ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ vaf = ctx->hwctx->funcs; ++ if (!vaf) ++ return 0; ++ + if (ctx->va_context != VA_INVALID_ID) { +- vas = vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "context %#x: %d (%s).\n", +- ctx->va_context, vas, vaErrorStr(vas)); ++ ctx->va_context, vas, vaf->vaErrorStr(vas)); + } + } + if (ctx->va_config != VA_INVALID_ID) { +- vas = vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ vas = vaf->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "configuration %#x: %d (%s).\n", +- ctx->va_config, vas, vaErrorStr(vas)); ++ ctx->va_config, vas, vaf->vaErrorStr(vas)); + } + } + +diff --git a/libavcodec/vaapi_encode.c b/libavcodec/vaapi_encode.c +index 16a9a364f0..ccf6fa59d6 100644 +--- a/libavcodec/vaapi_encode.c ++++ b/libavcodec/vaapi_encode.c +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID param_buffer, data_buffer; + VABufferID *tmp; +@@ -57,24 +58,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, + return AVERROR(ENOMEM); + pic->param_buffers = tmp; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderParameterBufferType, + sizeof(params), 1, ¶ms, ¶m_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = param_buffer; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderDataBufferType, + (bit_len + 7) / 8, 1, data, &data_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create data buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = data_buffer; +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID *tmp; + VABufferID buffer; +@@ -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, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + 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)); ++ "(type %d): %d (%s).\n", type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = buffer; +@@ -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; + +@@ -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 (base_ctx->async_encode) { +- vas = vaSyncBuffer(ctx->hwctx->display, ++ if (base_ctx->async_encode && vaf->vaSyncBuffer) { ++ vas = vaf->vaSyncBuffer(ctx->hwctx->display, + pic->output_buffer, + VA_TIMEOUT_INFINITE); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to output buffer completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } else + #endif + { // If vaSyncBuffer is not implemented, try old version API. +- vas = vaSyncSurface(ctx->hwctx->display, pic->input_surface); ++ vas = vaf->vaSyncSurface(ctx->hwctx->display, pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to picture completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -270,6 +273,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + { + 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; +@@ -587,28 +591,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + } + #endif + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture encode issue: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload encode parameters: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + 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)); + err = AVERROR(EIO); + // vaRenderPicture() has been called here, so we should not destroy + // the parameter buffers unless separate destruction is required. +@@ -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++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "param buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + // And ignore. + } + } +@@ -636,10 +640,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + return 0; + + fail_with_picture: +- vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + fail: + for(i = 0; i < pic->nb_param_buffers; i++) +- vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); + if (pic->slices) { + for (i = 0; i < pic->nb_slices; i++) + av_freep(&pic->slices[i].codec_slice_params); +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + int size = 0; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -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; + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -710,10 +716,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, + *dst += buf->size; + } + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -936,6 +942,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAProfile *va_profiles = NULL; + VAEntrypoint *va_entrypoints = NULL; + VAStatus vas; +@@ -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); + +- n = vaMaxNumProfiles(ctx->hwctx->display); ++ n = vaf->vaMaxNumProfiles(ctx->hwctx->display); + va_profiles = av_malloc_array(n, sizeof(VAProfile)); + if (!va_profiles) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); ++ vas = vaf->vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: %d (%s).\n", +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1007,7 +1014,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + continue; + + #if VA_CHECK_VERSION(1, 0, 0) +- profile_string = vaProfileStr(profile->va_profile); ++ profile_string = vaf->vaProfileStr(profile->va_profile); + #else + profile_string = "(no profile names)"; + #endif +@@ -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); + +- n = vaMaxNumEntrypoints(ctx->hwctx->display); ++ n = vaf->vaMaxNumEntrypoints(ctx->hwctx->display); + va_entrypoints = av_malloc_array(n, sizeof(VAEntrypoint)); + if (!va_entrypoints) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, + va_entrypoints, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query entrypoints for " + "profile %s (%d): %d (%s).\n", profile_string, +- ctx->va_profile, vas, vaErrorStr(vas)); ++ ctx->va_profile, vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -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) +- entrypoint_string = vaEntrypointStr(ctx->va_entrypoint); ++ entrypoint_string = vaf->vaEntrypointStr(ctx->va_entrypoint); + #else + entrypoint_string = "(no entrypoint names)"; + #endif +@@ -1095,12 +1102,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + } + + rt_format_attr = (VAConfigAttrib) { VAConfigAttribRTFormat }; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rt_format_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query RT format " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + uint32_t supported_va_rc_modes; + const VAAPIEncodeRCMode *rc_mode; + int64_t rc_bits_per_second; +@@ -1170,12 +1178,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) + VAStatus vas; + char supported_rc_modes_string[64]; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rc_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query rate control " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + if (rc_attr.value == VA_ATTRIB_NOT_SUPPORTED) { +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr = { VAConfigAttribMaxFrameSize }; + VAStatus vas; + +@@ -1526,14 +1535,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + return AVERROR(EINVAL); + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + ctx->max_frame_size = 0; + av_log(avctx, AV_LOG_ERROR, "Failed to query max frame size " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1573,18 +1582,19 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + { + 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, err; + +- 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 reference frames " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -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, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_WARNING, "Failed to query prediction direction " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { + av_log(avctx, AV_LOG_VERBOSE, "Driver does not report any additional " +@@ -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) +@@ -1789,13 +1800,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + return 0; + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + attr, FF_ARRAY_ELEMS(attr)); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query slice " +- "attributes: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attributes: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + max_slices = attr[0].value; +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncPackedHeaders }; + +- 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 packed headers " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncQualityRange }; + int quality = avctx->compression_level; + +- 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 quality " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1958,16 +1971,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) + #if VA_CHECK_VERSION(1, 0, 0) + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncROI }; + +- 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 ROI " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1992,10 +2006,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, + { + AVCodecContext *avctx = opaque.nc; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id_ref = obj; + VABufferID buffer_id = *buffer_id_ref; + +- vaDestroyBuffer(ctx->hwctx->display, buffer_id); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, buffer_id); + + av_log(avctx, AV_LOG_DEBUG, "Freed output buffer %#x\n", buffer_id); + } +@@ -2005,6 +2020,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) + AVCodecContext *avctx = opaque.nc; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id = obj; + VAStatus vas; + +@@ -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 * 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 " +- "output buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "output buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOMEM); + } + +@@ -2092,6 +2108,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + AVVAAPIFramesContext *recon_hwctx = NULL; + VAStatus vas; + int err; +@@ -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); ++ goto fail; ++ } ++ vaf = ctx->hwctx->funcs; ++ + 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; + } + +- vas = vaCreateConfig(ctx->hwctx->display, ++ vas = vaf->vaCreateConfig(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + ctx->config_attributes, ctx->nb_config_attributes, + &ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -2173,7 +2196,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + goto fail; + + recon_hwctx = base_ctx->recon_frames->hwctx; +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + base_ctx->surface_width, base_ctx->surface_height, + VA_PROGRESSIVE, + recon_hwctx->surface_ids, +@@ -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 " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -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) { +- 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) { ++ 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 + +@@ -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) { +- if (ctx->hwctx) +- vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + ctx->va_context = VA_INVALID_ID; + } + + if (ctx->va_config != VA_INVALID_ID) { +- if (ctx->hwctx) +- vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + ctx->va_config = VA_INVALID_ID; + } + +diff --git a/libavcodec/vaapi_encode_h264.c b/libavcodec/vaapi_encode_h264.c +index fb87b68bec..6d4ce630ce 100644 +--- a/libavcodec/vaapi_encode_h264.c ++++ b/libavcodec/vaapi_encode_h264.c +@@ -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; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH264Context *priv = avctx->priv_data; + int err; + +@@ -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)); + +- driver = vaQueryVendorString(ctx->hwctx->display); ++ driver = vaf->vaQueryVendorString(ctx->hwctx->display); + if (!driver) + driver = "unknown driver"; + +diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c +index 2283bcc0b4..7c624f99a9 100644 +--- a/libavcodec/vaapi_encode_h265.c ++++ b/libavcodec/vaapi_encode_h265.c +@@ -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) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; ++ VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH265Context *priv = avctx->priv_data; + + #if VA_CHECK_VERSION(1, 13, 0) +@@ -909,7 +911,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + VAStatus vas; + + attr.type = VAConfigAttribEncHEVCFeatures; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ 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 encoder " +@@ -923,7 +925,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + } + + attr.type = VAConfigAttribEncHEVCBlockSizes; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ 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 encoder " +diff --git a/libavutil/hwcontext_vaapi.c b/libavutil/hwcontext_vaapi.c +index 95aa38d9d2..13451e8ad7 100644 +--- a/libavutil/hwcontext_vaapi.c ++++ b/libavutil/hwcontext_vaapi.c +@@ -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 +61,128 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) + #include "pixdesc.h" + #include "pixfmt.h" + ++//////////////////////////////////////////////////////////// ++/// dynamic load functions ++//////////////////////////////////////////////////////////// ++ ++#define LOAD_SYMBOL(name) do { \ ++ funcs->name = dlsym(funcs->handle_va, #name); \ ++ if (!funcs->name) { \ ++ av_log(NULL, AV_LOG_ERROR, "Failed to load %s\n", #name); \ ++ goto fail; \ ++ } \ ++} while(0) ++ ++static void vaapi_free_functions(VAAPIDynLoadFunctions *funcs) ++{ ++ if (!funcs) ++ return; ++ ++ if (funcs->handle_va_x11) ++ dlclose(funcs->handle_va_x11); ++ if (funcs->handle_va_drm) ++ dlclose(funcs->handle_va_drm); ++ if (funcs->handle_va) ++ dlclose(funcs->handle_va); ++ av_free(funcs); ++} ++ ++static VAAPIDynLoadFunctions *vaapi_load_functions(void) ++{ ++ VAAPIDynLoadFunctions *funcs = av_mallocz(sizeof(*funcs)); ++ if (!funcs) ++ return NULL; ++ ++ // Load libva.so ++ funcs->handle_va = dlopen("libva.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ // Load core functions ++ LOAD_SYMBOL(vaInitialize); ++ LOAD_SYMBOL(vaTerminate); ++ LOAD_SYMBOL(vaCreateConfig); ++ LOAD_SYMBOL(vaDestroyConfig); ++ LOAD_SYMBOL(vaCreateContext); ++ LOAD_SYMBOL(vaDestroyContext); ++ LOAD_SYMBOL(vaCreateBuffer); ++ LOAD_SYMBOL(vaDestroyBuffer); ++ LOAD_SYMBOL(vaMapBuffer); ++ LOAD_SYMBOL(vaUnmapBuffer); ++ LOAD_SYMBOL(vaSyncSurface); ++ LOAD_SYMBOL(vaGetConfigAttributes); ++ LOAD_SYMBOL(vaCreateSurfaces); ++ LOAD_SYMBOL(vaDestroySurfaces); ++ LOAD_SYMBOL(vaBeginPicture); ++ LOAD_SYMBOL(vaRenderPicture); ++ LOAD_SYMBOL(vaEndPicture); ++ LOAD_SYMBOL(vaQueryConfigEntrypoints); ++ LOAD_SYMBOL(vaQueryConfigProfiles); ++ LOAD_SYMBOL(vaGetDisplayAttributes); ++ LOAD_SYMBOL(vaErrorStr); ++ LOAD_SYMBOL(vaMaxNumEntrypoints); ++ LOAD_SYMBOL(vaMaxNumProfiles); ++ LOAD_SYMBOL(vaQueryVendorString); ++ LOAD_SYMBOL(vaQuerySurfaceAttributes); ++ LOAD_SYMBOL(vaDestroyImage); ++ LOAD_SYMBOL(vaDeriveImage); ++ LOAD_SYMBOL(vaPutImage); ++ LOAD_SYMBOL(vaCreateImage); ++ LOAD_SYMBOL(vaGetImage); ++ LOAD_SYMBOL(vaExportSurfaceHandle); ++ LOAD_SYMBOL(vaReleaseBufferHandle); ++ LOAD_SYMBOL(vaAcquireBufferHandle); ++ LOAD_SYMBOL(vaSetErrorCallback); ++ LOAD_SYMBOL(vaSetInfoCallback); ++ LOAD_SYMBOL(vaSetDriverName); ++ LOAD_SYMBOL(vaEntrypointStr); ++ LOAD_SYMBOL(vaQueryImageFormats); ++ LOAD_SYMBOL(vaMaxNumImageFormats); ++ LOAD_SYMBOL(vaProfileStr); ++ ++ // Load libva-x11.so ++ funcs->handle_va_x11 = dlopen("libva-x11.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_x11) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-x11: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplay = dlsym(funcs->handle_va_x11, "vaGetDisplay"); ++ if (!funcs->vaGetDisplay) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplay\n"); ++ goto fail; ++ } ++ ++ // Load libva-drm.so ++ funcs->handle_va_drm = dlopen("libva-drm.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_drm) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-drm: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplayDRM = dlsym(funcs->handle_va_drm, "vaGetDisplayDRM"); ++ if (!funcs->vaGetDisplayDRM) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplayDRM\n"); ++ goto fail; ++ } ++ ++ // Optional functions ++ funcs->vaSyncBuffer = dlsym(funcs->handle_va, "vaSyncBuffer"); ++ av_log(NULL, AV_LOG_DEBUG, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); ++ ++ return funcs; ++ ++fail: ++ vaapi_free_functions(funcs); ++ return NULL; ++} ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// ++ + + typedef struct VAAPIDevicePriv { + #if HAVE_VAAPI_X11 +@@ -236,6 +359,7 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const AVVAAPIHWConfig *config = hwconfig; + VASurfaceAttrib *attr_list = NULL; + VAStatus vas; +@@ -246,11 +370,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + if (config && + !(hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES)) { + attr_count = 0; +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + 0, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -261,11 +385,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + goto fail; + } + +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + attr_list, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -396,6 +520,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAImageFormat *image_list = NULL; + VAStatus vas; + const char *vendor_string; +@@ -403,7 +528,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + enum AVPixelFormat pix_fmt; + unsigned int fourcc; + +- image_count = vaMaxNumImageFormats(hwctx->display); ++ image_count = vaf->vaMaxNumImageFormats(hwctx->display); + if (image_count <= 0) { + err = AVERROR(EIO); + goto fail; +@@ -413,7 +538,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryImageFormats(hwctx->display, image_list, &image_count); ++ vas = vaf->vaQueryImageFormats(hwctx->display, image_list, &image_count); + if (vas != VA_STATUS_SUCCESS) { + err = AVERROR(EIO); + goto fail; +@@ -440,7 +565,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + } + } + +- vendor_string = vaQueryVendorString(hwctx->display); ++ vendor_string = vaf->vaQueryVendorString(hwctx->display); + if (vendor_string) + av_log(hwdev, AV_LOG_VERBOSE, "VAAPI driver: %s.\n", vendor_string); + +@@ -493,15 +618,16 @@ static void vaapi_buffer_free(void *opaque, uint8_t *data) + { + AVHWFramesContext *hwfc = opaque; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)data; + +- vas = vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vas = vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +@@ -511,6 +637,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + AVBufferRef *ref; +@@ -519,13 +646,13 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + avfc->nb_surfaces >= hwfc->initial_pool_size) + return NULL; + +- vas = vaCreateSurfaces(hwctx->display, ctx->rt_format, ++ vas = vaf->vaCreateSurfaces(hwctx->display, ctx->rt_format, + hwfc->width, hwfc->height, + &surface_id, 1, + ctx->attributes, ctx->nb_attributes); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create surface: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return NULL; + } + av_log(hwfc, AV_LOG_DEBUG, "Created surface %#x.\n", surface_id); +@@ -534,7 +661,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + sizeof(surface_id), &vaapi_buffer_free, + hwfc, AV_BUFFER_FLAG_READONLY); + if (!ref) { +- vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + return NULL; + } + +@@ -554,6 +681,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const VAAPIFormatDescriptor *desc; + VAImageFormat *expected_format; + AVBufferRef *test_surface = NULL; +@@ -669,7 +797,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + err = vaapi_get_image_format(hwfc->device_ctx, + hwfc->sw_format, &expected_format); + if (err == 0) { +- vas = vaDeriveImage(hwctx->display, test_surface_id, &test_image); ++ vas = vaf->vaDeriveImage(hwctx->display, test_surface_id, &test_image); + if (vas == VA_STATUS_SUCCESS) { + if (expected_format->fourcc == test_image.format.fourcc) { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping possible.\n"); +@@ -680,11 +808,11 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + "expected format %08x.\n", + expected_format->fourcc, test_image.format.fourcc); + } +- vaDestroyImage(hwctx->display, test_image.image_id); ++ vaf->vaDestroyImage(hwctx->display, test_image.image_id); + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " + "deriving image does not work: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " +@@ -765,33 +893,34 @@ static void vaapi_unmap_frame(AVHWFramesContext *hwfc, + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; + VAAPIMapping *map = hwmap->priv; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)hwmap->source->data[3]; + av_log(hwfc, AV_LOG_DEBUG, "Unmap surface %#x.\n", surface_id); + +- vas = vaUnmapBuffer(hwctx->display, map->image.buf); ++ vas = vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to unmap image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + if ((map->flags & AV_HWFRAME_MAP_WRITE) && + !(map->flags & AV_HWFRAME_MAP_DIRECT)) { +- vas = vaPutImage(hwctx->display, surface_id, map->image.image_id, ++ vas = vaf->vaPutImage(hwctx->display, surface_id, map->image.image_id, + 0, 0, hwfc->width, hwfc->height, + 0, 0, hwfc->width, hwfc->height); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to write image to surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +- vas = vaDestroyImage(hwctx->display, map->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(map); +@@ -801,6 +930,7 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + AVFrame *dst, const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIFramesContext *ctx = hwfc->hwctx; + VASurfaceID surface_id; + const VAAPIFormatDescriptor *desc; +@@ -839,10 +969,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + map->flags = flags; + map->image.image_id = VA_INVALID_ID; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -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))) { +- vas = vaDeriveImage(hwctx->display, surface_id, &map->image); ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -873,41 +1003,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + } + map->flags |= AV_HWFRAME_MAP_DIRECT; + } else { +- vas = vaCreateImage(hwctx->display, image_format, ++ vas = vaf->vaCreateImage(hwctx->display, image_format, + hwfc->width, hwfc->height, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create image for " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + if (!(flags & AV_HWFRAME_MAP_OVERWRITE)) { +- vas = vaGetImage(hwctx->display, surface_id, 0, 0, ++ vas = vaf->vaGetImage(hwctx->display, surface_id, 0, 0, + hwfc->width, hwfc->height, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to read image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + } + } + +-#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 " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -936,9 +1057,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + fail: + if (map) { + if (address) +- vaUnmapBuffer(hwctx->display, map->image.buf); ++ vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (map->image.image_id != VA_INVALID_ID) +- vaDestroyImage(hwctx->display, map->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, map->image.image_id); + av_free(map); + } + return err; +@@ -1080,12 +1201,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; +- ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + VASurfaceID surface_id = (VASurfaceID)(uintptr_t)hwmap->priv; + + av_log(dst_fc, AV_LOG_DEBUG, "Destroy surface %#x.\n", surface_id); + +- vaDestroySurfaces(dst_dev->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(dst_dev->display, &surface_id, 1); + } + + 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; ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + const AVDRMFrameDescriptor *desc; + const VAAPIFormatDescriptor *format_desc; + VASurfaceID surface_id; +@@ -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. + */ +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, &surface_id, 1, + prime_attrs, FF_ARRAY_ELEMS(prime_attrs)); + if (vas != VA_STATUS_SUCCESS) +@@ -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]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + buffer_attrs, FF_ARRAY_ELEMS(buffer_attrs)); +@@ -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]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + attrs, FF_ARRAY_ELEMS(attrs)); + #endif + if (vas != VA_STATUS_SUCCESS) { + av_log(dst_fc, AV_LOG_ERROR, "Failed to create surface from DRM " +- "object: %d (%s).\n", vas, vaErrorStr(vas)); ++ "object: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(dst_fc, AV_LOG_DEBUG, "Create surface %#x.\n", surface_id); +@@ -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; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + VADRMPRIMESurfaceDescriptor va_desc; +@@ -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; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -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; + +- vas = vaExportSurfaceHandle(hwctx->display, surface_id, ++ vas = vaf->vaExportSurfaceHandle(hwctx->display, surface_id, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + export_flags, &va_desc); + if (vas != VA_STATUS_SUCCESS) { + if (vas == VA_STATUS_ERROR_UNIMPLEMENTED) + return AVERROR(ENOSYS); + av_log(hwfc, AV_LOG_ERROR, "Failed to export surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -1437,6 +1560,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = hwmap->priv; + VASurfaceID surface_id; + VAStatus vas; +@@ -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. + +- vas = vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vas = vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to release buffer " + "handle of image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + } + +- vas = vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image " + "derived from surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(mapping); +@@ -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; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = NULL; + VASurfaceID surface_id; + VAStatus vas; +@@ -1483,12 +1608,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + if (!mapping) + return AVERROR(ENOMEM); + +- vas = vaDeriveImage(hwctx->display, surface_id, ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, + &mapping->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -1543,13 +1668,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + } + } + +- vas = vaAcquireBufferHandle(hwctx->display, mapping->image.buf, ++ vas = vaf->vaAcquireBufferHandle(hwctx->display, mapping->image.buf, + &mapping->buffer_info); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to get buffer " + "handle from image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_derived; + } +@@ -1578,9 +1703,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + return 0; + + fail_mapped: +- vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + fail_derived: +- vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + fail: + av_freep(&mapping); + return err; +@@ -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->display) +- vaTerminate(hwctx->display); ++ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) ++ vaf->vaTerminate(hwctx->display); ++ ++ if (hwctx && hwctx->funcs) { ++ vaapi_free_functions(hwctx->funcs); ++ hwctx->funcs = NULL; ++ } + + #if HAVE_VAAPI_X11 + if (priv->x11_display) +@@ -1669,20 +1800,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, + VADisplay display) + { + AVVAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + int major, minor; + VAStatus vas; + + #if CONFIG_VAAPI_1 +- vaSetErrorCallback(display, &vaapi_device_log_error, ctx); +- vaSetInfoCallback (display, &vaapi_device_log_info, ctx); ++ vaf->vaSetErrorCallback(display, &vaapi_device_log_error, ctx); ++ vaf->vaSetInfoCallback (display, &vaapi_device_log_info, ctx); + #endif + + hwctx->display = display; + +- vas = vaInitialize(display, &major, &minor); ++ vas = vaf->vaInitialize(display, &major, &minor); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to initialise VAAPI " +- "connection: %d (%s).\n", vas, vaErrorStr(vas)); ++ "connection: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(ctx, AV_LOG_VERBOSE, "Initialised VAAPI connection: " +@@ -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; ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf; ++ ++ hwctx->p.funcs = vaapi_load_functions(); ++ if (!hwctx->p.funcs) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ return AVERROR_EXTERNAL; ++ } ++ ++ vaf = hwctx->p.funcs; + + priv = av_mallocz(sizeof(*priv)); + if (!priv) +@@ -1843,7 +1985,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + break; + } + +- display = vaGetDisplayDRM(priv->drm_fd); ++ display = vaf->vaGetDisplayDRM(priv->drm_fd); + if (!display) { + av_log(ctx, AV_LOG_VERBOSE, "Cannot open a VA display " + "from DRM device %s.\n", 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 { +- display = vaGetDisplay(priv->x11_display); ++ display = vaf->vaGetDisplay(priv->x11_display); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Cannot open a VA display " + "from X11 display %s.\n", XDisplayName(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; +- vas = vaSetDriverName(display, ent->value); ++ vas = vaf->vaSetDriverName(display, ent->value); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to set driver name to " +- "%s: %d (%s).\n", ent->value, vas, vaErrorStr(vas)); +- vaTerminate(display); ++ "%s: %d (%s).\n", ent->value, vas, vaf->vaErrorStr(vas)); ++ vaf->vaTerminate(display); + return AVERROR_EXTERNAL; + } + #else +@@ -1970,6 +2112,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + AVHWDeviceContext *src_ctx, + AVDictionary *opts, int flags) + { ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->p.funcs; + #if HAVE_VAAPI_DRM + if (src_ctx->type == AV_HWDEVICE_TYPE_DRM) { + AVDRMDeviceContext *src_hwctx = src_ctx->hwctx; +@@ -2041,7 +2185,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + ctx->user_opaque = priv; + ctx->free = &vaapi_device_free; + +- display = vaGetDisplayDRM(fd); ++ display = vaf->vaGetDisplayDRM(fd); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Failed to open a VA display from " + "DRM device.\n"); +diff --git a/libavutil/hwcontext_vaapi.h b/libavutil/hwcontext_vaapi.h +index 0b2e071cb3..2c51223d45 100644 +--- a/libavutil/hwcontext_vaapi.h ++++ b/libavutil/hwcontext_vaapi.h +@@ -20,6 +20,100 @@ + #define AVUTIL_HWCONTEXT_VAAPI_H + + #include ++#include ++#include ++#include ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI dynamic load functions start ++//////////////////////////////////////////////////////////// ++ ++typedef struct VAAPIDynLoadFunctions { ++ // Core VA functions ++ VAStatus (*vaInitialize)(VADisplay dpy, int *major_version, int *minor_version); ++ VAStatus (*vaTerminate)(VADisplay dpy); ++ VAStatus (*vaCreateConfig)(VADisplay dpy, VAProfile profile, VAEntrypoint entrypoint, ++ VAConfigAttrib *attrib_list, int num_attribs, VAConfigID *config_id); ++ VAStatus (*vaDestroyConfig)(VADisplay dpy, VAConfigID config_id); ++ VAStatus (*vaCreateContext)(VADisplay dpy, VAConfigID config_id, int picture_width, ++ int picture_height, int flag, VASurfaceID *render_targets, ++ int num_render_targets, VAContextID *context); ++ VAStatus (*vaDestroyContext)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaCreateBuffer)(VADisplay dpy, VAContextID context, VABufferType type, ++ unsigned int size, unsigned int num_elements, void *data, ++ VABufferID *buf_id); ++ VAStatus (*vaDestroyBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaMapBuffer)(VADisplay dpy, VABufferID buf_id, void **pbuf); ++ VAStatus (*vaUnmapBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaSyncSurface)(VADisplay dpy, VASurfaceID render_target); ++ VAStatus (*vaGetConfigAttributes)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint entrypoint, VAConfigAttrib *attrib_list, ++ int num_attribs); ++ VAStatus (*vaCreateSurfaces)(VADisplay dpy, unsigned int format, ++ unsigned int width, unsigned int height, ++ VASurfaceID *surfaces, unsigned int num_surfaces, ++ VASurfaceAttrib *attrib_list, unsigned int num_attribs); ++ VAStatus (*vaDestroySurfaces)(VADisplay dpy, VASurfaceID *surfaces, int num_surfaces); ++ VAStatus (*vaBeginPicture)(VADisplay dpy, VAContextID context, VASurfaceID render_target); ++ VAStatus (*vaRenderPicture)(VADisplay dpy, VAContextID context, ++ VABufferID *buffers, int num_buffers); ++ VAStatus (*vaEndPicture)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaQueryConfigEntrypoints)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint *entrypoint_list, int *num_entrypoints); ++ VAStatus (*vaQueryConfigProfiles)(VADisplay dpy, VAProfile *profile_list, int *num_profiles); ++ VAStatus (*vaGetDisplayAttributes)(VADisplay dpy, VADisplayAttribute *attr_list, int num_attributes); ++ const char *(*vaErrorStr)(VAStatus error_status); ++ int (*vaMaxNumEntrypoints)(VADisplay dpy); ++ int (*vaMaxNumProfiles)(VADisplay dpy); ++ const char *(*vaQueryVendorString)(VADisplay dpy); ++ VAStatus (*vaQuerySurfaceAttributes)(VADisplay dpy, VAConfigID config_id, ++ VASurfaceAttrib *attrib_list, int *num_attribs); ++ VAStatus (*vaDestroyImage)(VADisplay dpy, VAImageID image); ++ VAStatus (*vaDeriveImage)(VADisplay dpy, VASurfaceID surface, VAImage *image); ++ VAStatus (*vaPutImage)(VADisplay dpy, VASurfaceID surface, VAImageID image, ++ int src_x, int src_y, unsigned int src_width, unsigned int src_height, ++ int dest_x, int dest_y, unsigned int dest_width, unsigned int dest_height); ++ VAStatus (*vaCreateImage)(VADisplay dpy, VAImageFormat *format, int width, int height, VAImage *image); ++ VAStatus (*vaGetImage)(VADisplay dpy, VASurfaceID surface, ++ int x, int y, unsigned int width, unsigned int height, ++ VAImageID image); ++ VAStatus (*vaExportSurfaceHandle)(VADisplay dpy, VASurfaceID surface_id, ++ uint32_t mem_type, uint32_t flags, ++ void *descriptor); ++ VAStatus (*vaReleaseBufferHandle)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaAcquireBufferHandle)(VADisplay dpy, VABufferID buf_id, ++ VABufferInfo *buf_info); ++ VAStatus (*vaSetErrorCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetInfoCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetDriverName)(VADisplay dpy, const char *driver_name); ++ const char *(*vaEntrypointStr)(VAEntrypoint entrypoint); ++ VAStatus (*vaQueryImageFormats)(VADisplay dpy, VAImageFormat *format_list, int *num_formats); ++ int (*vaMaxNumImageFormats)(VADisplay dpy); ++ const char *(*vaProfileStr)(VAProfile profile); ++ ++ ++ // Optional functions ++ VAStatus (*vaSyncBuffer)(VADisplay dpy, VABufferID buf_id, uint64_t timeout_ns); ++ ++ // X11 specific functions ++ VADisplay (*vaGetDisplay)(Display *dpy); ++ ++ // DRM specific functions ++ VADisplay (*vaGetDisplayDRM)(int fd); ++ ++ ++ ++ // Library handles ++ void *handle_va; ++ void *handle_va_drm; ++ void *handle_va_x11; ++} VAAPIDynLoadFunctions; ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// + + /** + * @file +@@ -78,6 +172,8 @@ typedef struct AVVAAPIDeviceContext { + * operations using VAAPI with the same VADisplay. + */ + unsigned int driver_quirks; ++ ++ VAAPIDynLoadFunctions *funcs; + } AVVAAPIDeviceContext; + + /** +-- +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 000000000000..21a1f4d4fe73 --- /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 000000000000..fe08aebdadf9 --- /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 000000000000..2e8aff64aa5a --- /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 000000000000..18da50b446c0 --- /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 dc35752ff8ba..9d09c526423b 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -1,42 +1,33 @@ -if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - set(FF_VERSION "n5.1.5") - set(FF_SHA512 "a933f18e53207ccc277b42c9a68db00f31cefec555e6d5d7c57db3409023b2c38fd93ebe2ccfcd17ba2397adb912e93f2388241ca970b7d8bd005ccfe86d5679") -else() - set(FF_VERSION "n7.0.1") - set(FF_SHA512 "1212ebcb78fdaa103b0304373d374e41bf1fe680e1fa4ce0f60624857491c26b4dda004c490c3ef32d4a0e10f42ae6b54546f9f318e2dcfbaa116117f687bc88") -endif() - vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO ffmpeg/ffmpeg - REF "${FF_VERSION}" - SHA512 "${FF_SHA512}" + REF "n${VERSION}" + 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/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(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0004-amf-colorspace.patch - ) -elseif(VCPKG_TARGET_IS_ANDROID) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/7.0/0001-android-mediacodec-encode-align-64.patch - ) -endif() - if(SOURCE_PATH MATCHES " ") message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces") endif() @@ -70,6 +61,7 @@ set(OPTIONS "\ --disable-debug \ --disable-valgrind-backtrace \ --disable-large-tests \ +--disable-bzlib \ --disable-avdevice \ --enable-avcodec \ --enable-avformat \ @@ -99,13 +91,15 @@ else() endif() if(VCPKG_TARGET_IS_LINUX) - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --target-os=linux \ --enable-pthreads \ +--disable-vdpau \ ") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") else() - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --enable-cuda \ --enable-ffnvcodec \ --enable-encoder=h264_nvenc \ @@ -120,8 +114,9 @@ if(VCPKG_TARGET_IS_LINUX) --enable-encoder=h264_vaapi \ --enable-encoder=hevc_vaapi \ ") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --enable-cuda_llvm \ ") endif() @@ -130,6 +125,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS) string(APPEND OPTIONS "\ --target-os=win32 \ --toolchain=msvc \ +--cc=cl \ --enable-gpl \ --enable-d3d11va \ --enable-cuda \ @@ -148,7 +144,8 @@ elseif(VCPKG_TARGET_IS_WINDOWS) --enable-libmfx \ --enable-encoder=h264_qsv \ --enable-encoder=hevc_qsv \ -") +") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") set(LIB_MACHINE_ARG /machine:x86) string(APPEND OPTIONS " --arch=i686 --enable-cross-compile") @@ -211,6 +208,11 @@ endif() string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"") string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"") +if(VCPKG_TARGET_IS_WINDOWS) + string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") + string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") +endif() + # # Setup vcpkg toolchain set(prog_env "") @@ -220,7 +222,10 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER) set(ENV{CC} "${CC_filename}") string(APPEND OPTIONS " --cc=${CC_filename}") - # string(APPEND OPTIONS " --host_cc=${CC_filename}") ffmpeg not yet setup for cross builds? + if(VCPKG_HOST_IS_WINDOWS) + string(APPEND OPTIONS " --host_cc=${CC_filename}") + endif() + list(APPEND prog_env "${CC_path}") endif() @@ -291,6 +296,14 @@ if(VCPKG_DETECTED_CMAKE_STRIP) list(APPEND prog_env "${STRIP_path}") endif() +if(VCPKG_HOST_IS_WINDOWS) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16) + set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe") + list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16") +else() + # find_program(SHELL bash) +endif() + list(REMOVE_DUPLICATES prog_env) vcpkg_add_to_path(PREPEND ${prog_env}) diff --git a/res/vcpkg/ffmpeg/vcpkg.json b/res/vcpkg/ffmpeg/vcpkg.json index 61ff2c8b549f..0346bb585763 100644 --- a/res/vcpkg/ffmpeg/vcpkg.json +++ b/res/vcpkg/ffmpeg/vcpkg.json @@ -1,7 +1,7 @@ { "name": "ffmpeg", - "version": "7.0.1", - "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 96eab8717390..ac54eafd4c92 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 ca4a47d309bb..d19c5daca068 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/linux.cmake b/res/vcpkg/linux.cmake deleted file mode 100644 index 9496930d0456..000000000000 --- a/res/vcpkg/linux.cmake +++ /dev/null @@ -1,74 +0,0 @@ -if(NOT _VCPKG_LINUX_TOOLCHAIN) -set(_VCPKG_LINUX_TOOLCHAIN 1) -if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - set(CMAKE_CROSSCOMPILING OFF CACHE BOOL "") -endif() -set(CMAKE_SYSTEM_NAME Linux CACHE STRING "") -if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") - set(CMAKE_SYSTEM_PROCESSOR x86_64 CACHE STRING "") -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") - set(CMAKE_SYSTEM_PROCESSOR x86 CACHE STRING "") - string(APPEND VCPKG_C_FLAGS " -m32") - string(APPEND VCPKG_CXX_FLAGS " -m32") - string(APPEND VCPKG_LINKER_FLAGS " -m32") -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") - set(CMAKE_SYSTEM_PROCESSOR armv7l CACHE STRING "") - if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - if(NOT DEFINED CMAKE_CXX_COMPILER) - set(CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++") - endif() - if(NOT DEFINED CMAKE_C_COMPILER) - set(CMAKE_C_COMPILER "arm-linux-gnueabihf-gcc") - endif() - if(NOT DEFINED CMAKE_ASM_COMPILER) - set(CMAKE_ASM_COMPILER "arm-linux-gnueabihf-gcc") - endif() - if(NOT DEFINED CMAKE_ASM-ATT_COMPILER) - set(CMAKE_ASM-ATT_COMPILER "arm-linux-gnueabihf-as") - endif() - message(STATUS "Cross compiling arm on host ${CMAKE_HOST_SYSTEM_PROCESSOR}, use cross compiler: ${CMAKE_CXX_COMPILER}/${CMAKE_C_COMPILER}") - endif() -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") - set(CMAKE_SYSTEM_PROCESSOR aarch64 CACHE STRING "") - if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - if(NOT DEFINED CMAKE_CXX_COMPILER) - set(CMAKE_CXX_COMPILER "aarch64-linux-gnu-g++") - endif() - if(NOT DEFINED CMAKE_C_COMPILER) - set(CMAKE_C_COMPILER "aarch64-linux-gnu-gcc") - endif() - if(NOT DEFINED CMAKE_ASM_COMPILER) - set(CMAKE_ASM_COMPILER "aarch64-linux-gnu-gcc") - endif() - if(NOT DEFINED CMAKE_ASM-ATT_COMPILER) - set(CMAKE_ASM-ATT_COMPILER "aarch64-linux-gnu-as") - endif() - message(STATUS "Cross compiling arm64 on host ${CMAKE_HOST_SYSTEM_PROCESSOR}, use cross compiler: ${CMAKE_CXX_COMPILER}/${CMAKE_C_COMPILER}") - endif() -endif() - -get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE ) -if(NOT _CMAKE_IN_TRY_COMPILE) - string(APPEND CMAKE_C_FLAGS_INIT " -fPIC ${VCPKG_C_FLAGS} ") - string(APPEND CMAKE_CXX_FLAGS_INIT " -fPIC ${VCPKG_CXX_FLAGS} ") - string(APPEND CMAKE_C_FLAGS_DEBUG_INIT " ${VCPKG_C_FLAGS_DEBUG} ") - string(APPEND CMAKE_CXX_FLAGS_DEBUG_INIT " ${VCPKG_CXX_FLAGS_DEBUG} ") - string(APPEND CMAKE_C_FLAGS_RELEASE_INIT " ${VCPKG_C_FLAGS_RELEASE} ") - string(APPEND CMAKE_CXX_FLAGS_RELEASE_INIT " ${VCPKG_CXX_FLAGS_RELEASE} ") - - string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - if(VCPKG_CRT_LINKAGE STREQUAL "static") - string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT "-static ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT "-static ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT "-static ") - endif() - string(APPEND CMAKE_MODULE_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_MODULE_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") -endif() -endif() diff --git a/res/vcpkg/oboe-wrapper/CMakeLists.txt b/res/vcpkg/oboe-wrapper/CMakeLists.txt deleted file mode 100644 index 9d50a989498c..000000000000 --- a/res/vcpkg/oboe-wrapper/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(oboe_wrapper CXX) - -include(GNUInstallDirs) - -add_library(oboe_wrapper STATIC - oboe.cc -) - -target_include_directories(oboe_wrapper PRIVATE "${CURRENT_INSTALLED_DIR}/include") - -install(TARGETS oboe_wrapper - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/res/vcpkg/oboe-wrapper/oboe.cc b/res/vcpkg/oboe-wrapper/oboe.cc deleted file mode 100644 index a3c8238a7027..000000000000 --- a/res/vcpkg/oboe-wrapper/oboe.cc +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include -#include - -// I got link problem with std::mutex, so use pthread instead -class CThreadLock -{ -public: - CThreadLock(); - virtual ~CThreadLock(); - - void Lock(); - void Unlock(); - -private: - pthread_mutex_t mutexlock; -}; - -CThreadLock::CThreadLock() -{ - // init lock here - pthread_mutex_init(&mutexlock, 0); -} - -CThreadLock::~CThreadLock() -{ - // deinit lock here - pthread_mutex_destroy(&mutexlock); -} -void CThreadLock::Lock() -{ - // lock - pthread_mutex_lock(&mutexlock); -} -void CThreadLock::Unlock() -{ - // unlock - pthread_mutex_unlock(&mutexlock); -} - -class Player : public oboe::AudioStreamDataCallback -{ -public: - Player(int channels, int sample_rate) - { - this->channels = channels; - oboe::AudioStreamBuilder builder; - // The builder set methods can be chained for convenience. - builder.setSharingMode(oboe::SharingMode::Exclusive) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setChannelCount(channels) - ->setSampleRate(sample_rate) - ->setFormat(oboe::AudioFormat::Float) - ->setDataCallback(this) - ->openManagedStream(outStream); - // Typically, start the stream after querying some stream information, as well as some input from the user - outStream->requestStart(); - } - - ~Player() { - outStream->requestStop(); - } - - oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override - { - float *floatData = (float *)audioData; - int i = 0; - mtx.Lock(); - auto n = channels * numFrames; - for (; i < n && i < (int)buffer.size(); ++i, ++floatData) - { - *floatData = buffer.front(); - buffer.pop_front(); - } - mtx.Unlock(); - for (; i < n; ++i, ++floatData) - { - *floatData = 0; - } - return oboe::DataCallbackResult::Continue; - } - - void push(const float *v, int n) - { - mtx.Lock(); - for (auto i = 0; i < n; ++i, ++v) - buffer.push_back(*v); - // in case memory overuse - if (buffer.size() > 48 * 1024 * 120) - buffer.clear(); - mtx.Unlock(); - } - -private: - oboe::ManagedStream outStream; - int channels; - std::deque buffer; - CThreadLock mtx; -}; - -extern "C" -{ - void *create_oboe_player(int channels, int sample_rate) - { - return new Player(channels, sample_rate); - } - - void push_oboe_data(void *player, const float* v, int n) - { - static_cast(player)->push(v, n); - } - - void destroy_oboe_player(void *player) - { - delete static_cast(player); - } -} \ No newline at end of file diff --git a/res/vcpkg/oboe-wrapper/portfile.cmake b/res/vcpkg/oboe-wrapper/portfile.cmake deleted file mode 100644 index c83f5bcb1d3f..000000000000 --- a/res/vcpkg/oboe-wrapper/portfile.cmake +++ /dev/null @@ -1,8 +0,0 @@ -vcpkg_configure_cmake( - SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}" - OPTIONS - -DCURRENT_INSTALLED_DIR=${CURRENT_INSTALLED_DIR} - PREFER_NINJA -) - -vcpkg_cmake_install() diff --git a/res/vcpkg/oboe-wrapper/vcpkg.json b/res/vcpkg/oboe-wrapper/vcpkg.json deleted file mode 100644 index be497e1bb261..000000000000 --- a/res/vcpkg/oboe-wrapper/vcpkg.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "oboe-wrapper", - "version": "0", - "description": "None", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - }, - { - "name": "oboe", - "host": false - } - ] -} diff --git a/res/vcpkg/opus/portfile.cmake b/res/vcpkg/opus/portfile.cmake index 3e37dc1d8c22..b4288b2200cc 100644 --- a/res/vcpkg/opus/portfile.cmake +++ b/res/vcpkg/opus/portfile.cmake @@ -2,7 +2,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO xiph/opus REF "v${VERSION}" - SHA512 ba79ad035993e7bc4c09b7d77964ba913eb0b2be33305e8a04a8c49aaab21c4d96ac828e31ae45484896105851fdfc8c305c63c8400e4481dd76c62a1c12286b + SHA512 4ffefd9c035671024f9720c5129bfe395dea04f0d6b730041c2804e89b1db6e4d19633ad1ae58855afc355034233537361e707f26dc53adac916554830038fab HEAD_REF main PATCHES fix-pkgconfig-version.patch ) diff --git a/res/vcpkg/opus/vcpkg.json b/res/vcpkg/opus/vcpkg.json index 4edeaa1e042c..574879eed651 100644 --- a/res/vcpkg/opus/vcpkg.json +++ b/res/vcpkg/opus/vcpkg.json @@ -1,6 +1,6 @@ { "name": "opus", - "version": "1.5.1", + "version": "1.5.2", "description": "Totally open, royalty-free, highly versatile audio codec", "homepage": "https://github.com/xiph/opus", "license": "BSD-3-Clause", diff --git a/src/auth_2fa.rs b/src/auth_2fa.rs index 6945bf461932..1c243bc77646 100644 --- a/src/auth_2fa.rs +++ b/src/auth_2fa.rs @@ -4,7 +4,7 @@ use hbb_common::{ config::Config, get_time, password_security::{decrypt_vec_or_original, encrypt_vec_or_original}, - tokio, ResultType, + ResultType, }; use serde_derive::{Deserialize, Serialize}; use std::sync::Mutex; @@ -165,9 +165,7 @@ pub async fn send_2fa_code_to_telegram(text: &str, bot: TelegramBot) -> ResultTy pub fn get_chatid_telegram(bot_token: &str) -> ResultType> { let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token); // because caller is in tokio runtime, so we must call post_request_sync in new thread. - let handle = std::thread::spawn(move || { - crate::post_request_sync(url, "".to_owned(), "") - }); + let handle = std::thread::spawn(move || crate::post_request_sync(url, "".to_owned(), "")); let resp = handle.join().map_err(|_| anyhow!("Thread panicked"))??; let value = serde_json::from_str::(&resp).map_err(|e| anyhow!(e))?; diff --git a/src/client.rs b/src/client.rs index e5823e187d13..bdef9c7ed0fe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,24 +1,29 @@ -use std::{ - collections::HashMap, - ffi::c_void, - net::SocketAddr, - ops::Deref, - str::FromStr, - sync::{mpsc, Arc, Mutex, RwLock}, -}; - use async_trait::async_trait; use bytes::Bytes; -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use clipboard_master::{CallbackResult, ClipboardHandler}; +#[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(any(target_os = "android", target_os = "linux")))] +#[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; -use sha2::{Digest, Sha256}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ffi::c_void, + io, + net::SocketAddr, + ops::Deref, + str::FromStr, + sync::{ + mpsc::{self, RecvTimeoutError, Sender}, + Arc, Mutex, RwLock, + }, +}; use uuid::Uuid; pub use file_trait::FileManager; @@ -32,13 +37,14 @@ use hbb_common::{ bail, config::{ self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, - PUBLIC_RS_PUB_KEY, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, + READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::{Message as _, MessageField}, rand, rendezvous_proto::*, + sha2::{Digest, Sha256}, socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6}, sodiumoxide::{base64, crypto::sign}, tcp::FramedStream, @@ -53,19 +59,21 @@ pub use helper::*; use scrap::{ codec::Decoder, record::{Recorder, RecorderContext}, - CodecFormat, ImageFormat, ImageRgb, + 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_buildin_option, use_texture_render}, + 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")))] -use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -79,7 +87,7 @@ pub mod io_loop; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); pub const VIDEO_QUEUE_SIZE: usize = 120; -const MAX_DECODE_FAIL_COUNTER: usize = 10; // Currently, failed decode cause refresh_video, so make it small +const MAX_DECODE_FAIL_COUNTER: usize = 3; #[cfg(target_os = "linux")] pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; @@ -108,6 +116,9 @@ 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")] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub(crate) struct ClientClipboardContext; @@ -122,13 +133,13 @@ pub(crate) struct ClientClipboardContext { /// Client of the remote desktop. pub struct Client; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] struct TextClipboardState { is_required: bool, running: bool, } -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -136,18 +147,15 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); - static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); +} + +#[cfg(not(target_os = "ios"))] +lazy_static::lazy_static! { static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_old_clipboard_text() -> Arc> { - OLD_CLIPBOARD_DATA.clone() -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { use enigo::KeyboardControllable; @@ -158,66 +166,6 @@ pub fn get_key_state(key: enigo::Key) -> bool { ENIGO.lock().unwrap().get_key_state(key) } -cfg_if::cfg_if! { - if #[cfg(target_os = "android")] { - -use hbb_common::libc::{c_float, c_int}; -type Oboe = *mut c_void; -extern "C" { - fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; - fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int); - fn destroy_oboe_player(oboe: Oboe); -} - -struct OboePlayer { - raw: Oboe, -} - -impl Default for OboePlayer { - fn default() -> Self { - Self { - raw: std::ptr::null_mut(), - } - } -} - -impl OboePlayer { - fn new(channels: i32, sample_rate: i32) -> Self { - unsafe { - Self { - raw: create_oboe_player(channels, sample_rate), - } - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn is_null(&self) -> bool { - self.raw.is_null() - } - - fn push(&mut self, d: &[f32]) { - if self.raw.is_null() { - return; - } - unsafe { - push_oboe_data(self.raw, d.as_ptr(), d.len() as _); - } - } -} - -impl Drop for OboePlayer { - fn drop(&mut self) { - unsafe { - if !self.raw.is_null() { - destroy_oboe_player(self.raw); - } - } - } -} - -} -} - impl Client { /// Start a new connection. pub async fn start( @@ -707,13 +655,20 @@ impl Client { #[inline] #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] fn try_stop_clipboard() { + // There's a bug here. + // If session is closed by the peer, `has_sessions_running()` will always return true. + // It's better to check if the active session number. + // But it's not a problem, because the clipboard thread does not consume CPU. + // + // If we want to fix it, we can add a flag to indicate if session is active. + // But I think it's not necessary to introduce complexity at this point. #[cfg(feature = "flutter")] if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; @@ -727,18 +682,44 @@ impl Client { // // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_start_clipboard(_ctx: Option) -> Option> { + fn try_start_clipboard( + _client_clip_ctx: Option, + ) -> Option> { let mut clipboard_lock = TEXT_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; + } + }; + clipboard_lock.running = true; - let (tx, rx) = unbounded_channel(); + + let (tx_started, rx_started) = unbounded_channel(); log::info!("Start text clipboard loop"); std::thread::spawn(move || { let mut is_sent = false; - let mut ctx = None; + loop { if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { break; @@ -748,43 +729,67 @@ impl Client { continue; } - if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) { - #[cfg(feature = "flutter")] - crate::flutter::send_text_clipboard_msg(msg); - #[cfg(not(feature = "flutter"))] - if let Some(ctx) = &_ctx { - if ctx.cfg.is_text_clipboard_required() { - let _ = ctx.tx.send(Data::Message(msg)); - } - } - } - if !is_sent { is_sent = true; - tx.send(()).ok(); + tx_started.send(()).ok(); } - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + log::error!("Clipboard listener stopped with error: {}", err); + break; + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} + } } log::info!("Stop text clipboard loop"); + shutdown.signal(); + h.join().ok(); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; }); - Some(rx) + Some(rx_started) } - #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn get_current_clipboard_msg() -> Option { - let data = &*OLD_CLIPBOARD_DATA.lock().unwrap(); - if data.is_empty() { - None - } else { - Some(data.create_msg()) + #[cfg(target_os = "android")] + fn try_start_clipboard(_p: Option<()>) -> Option> { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return None; } + clipboard_lock.running = true; + + log::info!("Start text clipboard loop"); + std::thread::spawn(move || { + loop { + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_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); + } + + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + } + log::info!("Stop text clipboard loop"); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + }); + + None } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] impl TextClipboardState { fn new() -> Self { Self { @@ -794,40 +799,206 @@ 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); + } + + #[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; + } + } + } + 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); + } + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + self.tx_cb_result + .send(CallbackResult::StopWithError(error)) + .ok(); + CallbackResult::Next + } +} + /// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, - #[cfg(target_os = "android")] - oboe: Option, #[cfg(target_os = "linux")] simple: Option, - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] audio_stream: Option>, channels: u16, - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] device_channel: u16, - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] ready: Arc>, } -#[cfg(not(any(target_os = "android", target_os = "linux")))] -struct AudioBuffer(pub Arc>>); +#[cfg(not(target_os = "linux"))] +struct AudioBuffer( + pub Arc>>, + usize, + [usize; 30], +); -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(target_os = "linux"))] impl Default for AudioBuffer { fn default() -> Self { - Self(Arc::new(std::sync::Mutex::new( - ringbuf::HeapRb::::new(48000 * 2), // 48000hz, 2 channel, 1 second - ))) + Self( + Arc::new(std::sync::Mutex::new( + ringbuf::HeapRb::::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel + )), + 48000 * 2, + [0; 30], + ) + } +} + +#[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; + let old_capacity = self.0.lock().unwrap().capacity(); + if capacity != old_capacity { + *self.0.lock().unwrap() = ringbuf::HeapRb::::new(capacity); + self.1 = sample_rate * channels; + log::info!("Audio buffer resized from {old_capacity} to {capacity}"); + } + } + + fn try_shrink(&mut self, having: usize) { + extern crate chrono; + use chrono::prelude::*; + + let mut i = (having * 10) / self.1; + if i > 29 { + i = 29; + } + self.2[i] += 1; + + #[allow(non_upper_case_globals)] + static mut tms: i64 = 0; + let dt = Local::now().timestamp_millis(); + unsafe { + if tms == 0 { + tms = dt; + return; + } else if dt < tms + 12000 { + return; + } + tms = dt; + } + + // the safer water mark to drop + let mut zero = 0; + // the water mark taking most of time + let mut max = 0; + for i in 0..30 { + if self.2[i] == 0 && zero == i { + zero += 1; + } + + if self.2[i] > self.2[max] { + self.2[max] = 0; + max = i; + } else { + self.2[i] = 0; + } + } + zero = zero * 2 / 3; + + // how many data can be dropped: + // 1. will not drop if buffered data is less than 600ms + // 2. choose based on min(zero, max) + const N: usize = 4; + self.2[max] = 0; + if max < 6 { + return; + } else if max > zero * N { + max = zero * N; + } + + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + let having = lock.occupied_len(); + let skip = (cap * max / (30 * N) + 1) & (!1); + if (having > skip * 3) && (skip > 0) { + lock.skip(skip); + log::info!("skip {skip}, based {max} {zero}"); + } + } + + /// append pcm to audio buffer, if buffered data + /// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS + /// will be kept. + fn append_pcm2(&self, buffer: &[f32]) -> usize { + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + if buffer.len() > cap { + lock.push_slice_overwrite(buffer); + return cap; + } + + let having = lock.occupied_len() + buffer.len(); + if having > cap { + lock.skip(having - cap); + } + lock.push_slice_overwrite(buffer); + lock.occupied_len() + } + + /// append pcm to audio buffer, trying to drop data + /// when data is too much (per 12 seconds) based + /// statistics. + pub fn append_pcm(&mut self, buffer: &[f32]) { + let having = self.append_pcm2(buffer); + self.try_shrink(having); } } impl AudioHandler { - /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { use psimple::Simple; @@ -858,18 +1029,7 @@ impl AudioHandler { } /// Start the audio playback. - #[cfg(target_os = "android")] - fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { - self.oboe = Some(OboePlayer::new( - format0.channels as _, - format0.sample_rate as _, - )); - self.sample_rate = (format0.sample_rate, format0.sample_rate); - Ok(()) - } - - /// Start the audio playback. - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -882,7 +1042,14 @@ impl AudioHandler { let sample_format = config.sample_format(); log::info!("Default output format: {:?}", config); log::info!("Remote input format: {:?}", format0); - let config: StreamConfig = config.into(); + #[allow(unused_mut)] + let mut config: StreamConfig = config.into(); + #[cfg(not(target_os = "ios"))] + { + // this makes ios audio output not work + config.buffer_size = cpal::BufferSize::Fixed(64); + } + self.sample_rate = (format0.sample_rate, config.sample_rate.0); let mut build_output_stream = |config: StreamConfig| match sample_format { cpal::SampleFormat::I8 => self.build_output_stream::(&config, &device), @@ -930,7 +1097,7 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } @@ -939,19 +1106,14 @@ impl AudioHandler { log::debug!("PulseAudio simple binding does not exists"); return; } - #[cfg(target_os = "android")] - if self.oboe.is_none() { - 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(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; - let audio_buffer = self.audio_buffer.0.clone(); let mut buffer = buffer[0..n].to_owned(); if sample_rate != sample_rate0 { buffer = crate::audio_resample( @@ -970,11 +1132,7 @@ impl AudioHandler { self.device_channel, ); } - audio_buffer.lock().unwrap().push_slice_overwrite(&buffer); - } - #[cfg(target_os = "android")] - { - self.oboe.as_mut().map(|x| x.push(&buffer[0..n])); + self.audio_buffer.append_pcm(&buffer); } #[cfg(target_os = "linux")] { @@ -987,7 +1145,7 @@ impl AudioHandler { } /// Build audio output stream for current device. - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(target_os = "linux"))] fn build_output_stream>( &mut self, config: &StreamConfig, @@ -998,23 +1156,53 @@ impl AudioHandler { // too many errors, will improve later log::trace!("an error occurred on stream: {}", err); }; + self.audio_buffer + .resize(config.sample_rate.0 as _, config.channels as _); let audio_buffer = self.audio_buffer.0.clone(); let ready = self.ready.clone(); let timeout = None; let stream = device.build_output_stream( config, - move |data: &mut [T], _: &_| { + move |data: &mut [T], info: &cpal::OutputCallbackInfo| { if !*ready.lock().unwrap() { *ready.lock().unwrap() = true; } - let mut lock = audio_buffer.lock().unwrap(); + let mut n = data.len(); - if lock.occupied_len() < n { - n = lock.occupied_len(); + let mut lock = audio_buffer.lock().unwrap(); + let mut having = lock.occupied_len(); + // android two timestamps, one from zero, another not + #[cfg(not(target_os = "android"))] + if having < n { + let tms = info.timestamp(); + let how_long = tms + .playback + .duration_since(&tms.callback) + .unwrap_or(Duration::from_millis(0)); + + // must long enough to fight back scheuler delay + if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000) + { + drop(lock); + std::thread::sleep(how_long.div_f32(1.2)); + lock = audio_buffer.lock().unwrap(); + having = lock.occupied_len(); + } + + if having < n { + n = having; + } + } + #[cfg(target_os = "android")] + if having < n { + n = having; } let mut elems = vec![0.0f32; n]; - lock.pop_slice(&mut elems); + if n > 0 { + lock.pop_slice(&mut elems); + } drop(lock); + let mut input = elems.into_iter(); for sample in data.iter_mut() { *sample = match input.next() { @@ -1036,11 +1224,12 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, pub rgb: ImageRgb, - pub texture: *mut c_void, + pub texture: ImageTexture, recorder: Arc>>, record: bool, _display: usize, // useful for debug fail_counter: usize, + first_frame: bool, } impl VideoHandler { @@ -1058,14 +1247,21 @@ impl VideoHandler { pub fn new(format: CodecFormat, _display: usize) -> Self { let luid = Self::get_adapter_luid(); log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}"); + let rgba_format = + if cfg!(feature = "flutter") && (cfg!(windows) || cfg!(target_os = "linux")) { + ImageFormat::ABGR + } else { + ImageFormat::ARGB + }; VideoHandler { decoder: Decoder::new(format, luid), - rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()), - texture: std::ptr::null_mut(), + rgb: ImageRgb::new(rgba_format, crate::get_dst_align_rgba()), + texture: Default::default(), recorder: Default::default(), record: false, _display, fail_counter: 0, + first_frame: true, } } @@ -1094,15 +1290,28 @@ impl VideoHandler { self.fail_counter = 0; } else { if self.fail_counter < usize::MAX { - self.fail_counter += 1 + if self.first_frame && self.fail_counter < MAX_DECODE_FAIL_COUNTER { + log::error!("decode first frame failed"); + self.fail_counter = MAX_DECODE_FAIL_COUNTER; + } else { + self.fail_counter += 1; + } + log::error!( + "Failed to handle video frame, fail counter: {}", + self.fail_counter + ); } } + self.first_frame = false; if self.record { - self.recorder - .lock() - .unwrap() - .as_mut() - .map(|r| r.write_frame(frame)); + self.recorder.lock().unwrap().as_mut().map(|r| { + let (w, h) = if *pixelbuffer { + (self.rgb.w, self.rgb.h) + } else { + (self.texture.w, self.texture.h) + }; + r.write_frame(frame, w, h).ok(); + }); } res } @@ -1112,26 +1321,28 @@ impl VideoHandler { /// Reset the decoder, change format if it is Some pub fn reset(&mut self, format: Option) { + log::info!( + "reset video handler for display #{}, format: {format:?}", + self._display + ); #[cfg(target_os = "macos")] self.rgb.set_align(crate::get_dst_align_rgba()); let luid = Self::get_adapter_luid(); let format = format.unwrap_or(self.decoder.format()); self.decoder = Decoder::new(format, luid); self.fail_counter = 0; + self.first_frame = true; } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) { + pub fn record_screen(&mut self, start: bool, id: String, display: usize) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - filename: "".to_owned(), - width: w as _, - height: h as _, - format: scrap::CodecFormat::VP9, + display, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -1144,7 +1355,7 @@ impl VideoHandler { } // The source of sent password -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] enum PasswordSource { PersonalAb(Vec), SharedAb(String), @@ -1190,6 +1401,13 @@ impl PasswordSource { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ConnToken { + password: Vec, + password_source: PasswordSource, + session_id: u64, +} + /// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { @@ -1219,6 +1437,9 @@ pub struct LoginConfigHandler { pub peer_info: Option, password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password + pub enable_trusted_devices: bool, + pub record_state: bool, + pub record_permission: bool, } impl Deref for LoginConfigHandler { @@ -1244,6 +1465,7 @@ impl LoginConfigHandler { mut force_relay: bool, adapter_luid: Option, shared_password: Option, + conn_token: Option, ) { let mut id = id; if id.contains("@") { @@ -1253,18 +1475,18 @@ 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 + config::RS_PUB_KEY.to_owned() } else { - let mut args_map: HashMap<&str, &str> = HashMap::new(); + let mut args_map: HashMap = HashMap::new(); for arg in args.split('&') { if let Some(kv) = arg.find('=') { - let k = &arg[0..kv]; + let k = arg[0..kv].to_lowercase(); let v = &arg[kv + 1..]; args_map.insert(k, v); } } let key = args_map.remove("key").unwrap_or_default(); - key + key.to_owned() }; // here we can check /r@server @@ -1272,7 +1494,7 @@ impl LoginConfigHandler { if real_id != raw_id { force_relay = true; } - self.other_server = Some((real_id.clone(), server.to_owned(), key.to_owned())); + self.other_server = Some((real_id.clone(), server.to_owned(), key)); id = format!("{real_id}@{server}"); } else { let real_id = crate::ui_interface::handle_relay_id(&id); @@ -1287,10 +1509,22 @@ impl LoginConfigHandler { let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; - let mut sid = rand::random(); + + let conn_token = conn_token + .map(|x| serde_json::from_str::(&x).ok()) + .flatten(); + let mut sid = 0; + if let Some(token) = conn_token { + sid = token.session_id; + self.password = token.password; // use as last password + self.password_source = token.password_source; + } if sid == 0 { - // you won the lottery - sid = 1; + sid = rand::random(); + if sid == 0 { + // you won the lottery + sid = 1; + } } self.session_id = sid; self.supported_encoding = Default::default(); @@ -1310,6 +1544,8 @@ impl LoginConfigHandler { self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; + self.record_state = false; + self.record_permission = true; } /// Check if the client should auto login. @@ -1672,28 +1908,6 @@ impl LoginConfigHandler { ) } - pub fn get_option_message_after_login(&self) -> Option { - if self.conn_type.eq(&ConnType::FILE_TRANSFER) - || self.conn_type.eq(&ConnType::PORT_FORWARD) - || self.conn_type.eq(&ConnType::RDP) - { - return None; - } - let mut n = 0; - let mut msg = OptionMessage::new(); - if self.version < hbb_common::get_version_number("1.2.4") { - if self.get_toggle_option("privacy-mode") { - msg.privacy_mode = BoolOption::Yes.into(); - n += 1; - } - } - if n > 0 { - Some(msg) - } else { - None - } - } - /// Parse the image quality option. /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. /// @@ -2027,15 +2241,36 @@ impl LoginConfigHandler { } else { (my_id, self.id.clone()) }; - let mut display_name = get_buildin_option(config::keys::OPTION_DISPLAY_NAME); + let mut display_name = get_builtin_option(config::keys::OPTION_DISPLAY_NAME); + if display_name.is_empty() { + display_name = + serde_json::from_str::(&LocalConfig::get_option("user_info")) + .map(|x| { + x.get("name") + .map(|x| x.as_str().unwrap_or_default()) + .unwrap_or_default() + .to_owned() + }) + .unwrap_or_default(); + } if display_name.is_empty() { display_name = crate::username(); } + #[cfg(not(target_os = "android"))] + let my_platform = whoami::platform().to_string(); + #[cfg(target_os = "android")] + let my_platform = "Android".into(); + let hwid = if self.get_option("trust-this-device") == "Y" { + crate::get_hwid() + } else { + Bytes::new() + }; let mut lr = LoginRequest { username: pure_id, password: password.into(), my_id, my_name: display_name, + my_platform, option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), @@ -2045,6 +2280,7 @@ impl LoginConfigHandler { ..Default::default() }) .into(), + hwid, ..Default::default() }; match self.conn_type { @@ -2090,78 +2326,76 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn get_conn_token(&self) -> Option { + if self.password.is_empty() { + return None; + } + serde_json::to_string(&ConnToken { + password: self.password.clone(), + password_source: self.password_source.clone(), + session_id: self.session_id, + }) + .ok() + } } /// Media data. pub enum MediaData { - VideoQueue(usize), + VideoQueue, VideoFrame(Box), AudioFrame(Box), AudioFormat(AudioFormat), - Reset(Option), - RecordScreen(bool, usize, i32, i32, String), + Reset, + RecordScreen(bool), } pub type MediaSender = mpsc::Sender; -struct VideoHandlerController { - handler: VideoHandler, - skip_beginning: u32, -} - -/// Start video and audio thread. -/// Return two [`MediaSender`], they should be given to the media producer. +/// Start video thread. /// /// # Arguments /// /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. -pub fn start_video_audio_threads( +pub fn start_video_thread( session: Session, + display: usize, + video_receiver: mpsc::Receiver, + video_queue: Arc>>, + fps: Arc>>, + chroma: Arc>>, + discard_queue: Arc>, video_callback: F, -) -> ( - MediaSender, - MediaSender, - Arc>>>, - Arc>>, - Arc>>, -) -where +) where F: 'static + FnMut(usize, &mut scrap::ImageRgb, *mut c_void, bool) + Send, T: InvokeUiSession, { - let (video_sender, video_receiver) = mpsc::channel::(); - let video_queue_map: Arc>>> = Default::default(); - let video_queue_map_cloned = video_queue_map.clone(); let mut video_callback = video_callback; - - let fps = Arc::new(RwLock::new(None)); - let decode_fps_map = fps.clone(); - let chroma = Arc::new(RwLock::new(None)); - let chroma_cloned = chroma.clone(); let mut last_chroma = None; std::thread::spawn(move || { #[cfg(windows)] sync_cpu_usage(); get_hwcodec_config(); - let mut handler_controller_map = HashMap::new(); + let mut video_handler = None; let mut count = 0; let mut duration = std::time::Duration::ZERO; + let mut skip_beginning = 0; loop { if let Ok(data) = video_receiver.recv() { match data { - MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => { + MediaData::VideoFrame(_) | MediaData::VideoQueue => { let vf = match data { - MediaData::VideoFrame(vf) => *vf, - MediaData::VideoQueue(display) => { - if let Some(video_queue) = - video_queue_map.read().unwrap().get(&display) - { - if let Some(vf) = video_queue.pop() { - vf - } else { + MediaData::VideoFrame(vf) => { + *discard_queue.write().unwrap() = false; + *vf + } + MediaData::VideoQueue => { + if let Some(vf) = video_queue.read().unwrap().pop() { + if discard_queue.read().unwrap().clone() { continue; } + vf } else { continue; } @@ -2174,30 +2408,26 @@ where let display = vf.display as usize; let start = std::time::Instant::now(); let format = CodecFormat::from(&vf); - if !handler_controller_map.contains_key(&display) { - handler_controller_map.insert( - display, - VideoHandlerController { - handler: VideoHandler::new(format, display), - skip_beginning: 0, - }, - ); + if video_handler.is_none() { + let mut handler = VideoHandler::new(format, display); + 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_state && record_permission { + handler.record_screen(true, id, display); + } + video_handler = Some(handler); } - if let Some(handler_controller) = handler_controller_map.get_mut(&display) { + if let Some(handler) = video_handler.as_mut() { let mut pixelbuffer = true; let mut tmp_chroma = None; - let format_changed = - handler_controller.handler.decoder.format() != format; - match handler_controller.handler.handle_frame( - vf, - &mut pixelbuffer, - &mut tmp_chroma, - ) { + let format_changed = handler.decoder.format() != format; + match handler.handle_frame(vf, &mut pixelbuffer, &mut tmp_chroma) { Ok(true) => { video_callback( display, - &mut handler_controller.handler.rgb, - handler_controller.handler.texture, + &mut handler.rgb, + handler.texture.texture, pixelbuffer, ); @@ -2209,7 +2439,7 @@ where // fps calculation fps_calculate( - handler_controller, + &mut skip_beginning, &fps, format_changed, start.elapsed(), @@ -2238,52 +2468,34 @@ where // check invalid decoders let mut should_update_supported = false; - handler_controller_map - .iter() - .map(|(_, h)| { - if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER { - let mut lc = session.lc.write().unwrap(); - let format = h.handler.decoder.format(); - if !lc.mark_unsupported.contains(&format) { - lc.mark_unsupported.push(format); - should_update_supported = true; - log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", h.handler.decoder.valid(), h.handler.fail_counter, lc.mark_unsupported); - } + if let Some(handler) = video_handler.as_mut() { + if !handler.decoder.valid() + || handler.fail_counter >= MAX_DECODE_FAIL_COUNTER + { + let mut lc = session.lc.write().unwrap(); + let format = handler.decoder.format(); + if !lc.mark_unsupported.contains(&format) { + lc.mark_unsupported.push(format); + should_update_supported = true; + log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", handler.decoder.valid(), handler.fail_counter, lc.mark_unsupported); } - }) - .count(); + } + } if should_update_supported { session.send(Data::Message( session.lc.read().unwrap().update_supported_decodings(), )); } } - MediaData::Reset(display) => { - if let Some(display) = display { - if let Some(handler_controler) = - handler_controller_map.get_mut(&display) - { - handler_controler.handler.reset(None); - } - } else { - for (_, handler_controler) in handler_controller_map.iter_mut() { - handler_controler.handler.reset(None); - } + MediaData::Reset => { + if let Some(handler) = video_handler.as_mut() { + handler.reset(None); } } - MediaData::RecordScreen(start, display, w, h, id) => { - log::info!("record screen command: start: {start}, display: {display}"); - // Compatible with the sciter version(single ui session). - // For the sciter version, there're no multi-ui-sessions for one connection. - // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. - if let Some(handler_controler) = handler_controller_map.get_mut(&display) { - handler_controler.handler.record_screen(start, w, h, id); - } else if handler_controller_map.len() == 1 { - if let Some(handler_controler) = - handler_controller_map.values_mut().next() - { - handler_controler.handler.record_screen(start, w, h, id); - } + MediaData::RecordScreen(start) => { + let id = session.lc.read().unwrap().id.clone(); + if let Some(handler) = video_handler.as_mut() { + handler.record_screen(start, id, display); } } _ => {} @@ -2294,14 +2506,6 @@ where } log::info!("Video decoder loop exits"); }); - let audio_sender = start_audio_thread(); - return ( - video_sender, - audio_sender, - video_queue_map_cloned, - decode_fps_map, - chroma_cloned, - ); } /// Start an audio thread @@ -2333,7 +2537,7 @@ pub fn start_audio_thread() -> MediaSender { #[inline] fn fps_calculate( - handler_controller: &mut VideoHandlerController, + skip_beginning: &mut usize, fps: &Arc>>, format_changed: bool, elapsed: std::time::Duration, @@ -2343,11 +2547,11 @@ fn fps_calculate( if format_changed { *count = 0; *duration = std::time::Duration::ZERO; - handler_controller.skip_beginning = 0; + *skip_beginning = 0; } // // The first frame will be very slow - if handler_controller.skip_beginning < 3 { - handler_controller.skip_beginning += 1; + if *skip_beginning < 3 { + *skip_beginning += 1; return; } *duration += elapsed; @@ -2604,6 +2808,7 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { return; } let mut key_event = KeyEvent::new(); + key_event.mode = KeyboardMode::Legacy.into(); key_event.press = true; let mut msg_out = Message::new(); key_event.set_seq(p); @@ -2701,6 +2906,12 @@ pub fn handle_login_error( interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true } else if err == LOGIN_MSG_2FA_WRONG || err == REQUIRE_2FA { + let enabled = lc.read().unwrap().get_option("trust-this-device") == "Y"; + if enabled { + lc.write() + .unwrap() + .set_option("trust-this-device".to_string(), "".to_string()); + } interface.msgbox("input-2fa", err, "", ""); true } else if LOGIN_ERROR_MAP.contains_key(err) { @@ -2794,7 +3005,7 @@ pub async fn handle_hash( if password.is_empty() { let p = - crate::ui_interface::get_buildin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); + crate::ui_interface::get_builtin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); if !p.is_empty() { let mut hasher = Sha256::new(); hasher.update(p.clone()); @@ -3035,12 +3246,13 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), - RecordScreen(bool, usize, i32, i32, String), + RecordScreen(bool), ElevateDirect, ElevateWithLogon(String, String), NewVoiceCall, CloseVoiceCall, ResetDecoder(Option), + RenameFile((i32, String, String, bool)), } /// Keycode for key events. @@ -3148,6 +3360,7 @@ lazy_static::lazy_static! { ("VK_PRINT", Key::ControlKey(ControlKey::Print)), ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_SCROLL", Key::ControlKey(ControlKey::Scroll)), ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), ("VK_HELP", Key::ControlKey(ControlKey::Help)), @@ -3267,3 +3480,135 @@ async fn hc_connection_( } Ok(()) } + +pub mod peer_online { + use hbb_common::{ + anyhow::bail, + config::{Config, CONNECT_TIMEOUT, READ_TIMEOUT}, + log, + rendezvous_proto::*, + sleep, + socket_client::connect_tcp, + tcp::FramedStream, + ResultType, + }; + + pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_timeout = std::time::Duration::from_millis(3_000); + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + } + Err(e) => { + log::debug!("query onlines, {}", &e); + } + } + } + } + + 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(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + connect_tcp(online_server, CONNECT_TIMEOUT).await + } + + async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, + ) -> ResultType<(Vec, Vec)> { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + let mut socket = match create_online_stream().await { + Ok(s) => s, + Err(e) => { + log::debug!("Failed to create peers online stream, {e}"); + return Ok((vec![], ids.clone())); + } + }; + // TODO: Use long connections to avoid socket creation + // If we use a Arc>> to hold and reuse the previous socket, + // we may face the following error: + // An established connection was aborted by the software in your host machine. (os error 10053) + if let Err(e) = socket.send(&msg_out).await { + log::debug!("Failed to send peers online states query, {e}"); + return Ok((vec![], ids.clone())); + } + // 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 + { + match msg_in.union { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } else { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + } + + bail!("Failed to query online states, no online response"); + } + + #[cfg(test)] + mod tests { + use hbb_common::tokio; + + #[tokio::test] + async fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ) + .await; + } + } +} diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 49e3f23585fb..88f0b14a5d63 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,4 @@ -use hbb_common::{fs, message_proto::*, log}; +use hbb_common::{fs, log, message_proto::*}; use super::{Data, Interface}; @@ -7,7 +7,12 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +25,12 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + ))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { @@ -33,6 +43,18 @@ pub trait FileManager: Interface { self.send(Data::CancelJob(id)); } + fn read_empty_dirs(&self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_empty_dirs(ReadEmptyDirs { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + fn read_remote_dir(&self, path: String, include_hidden: bool) { let mut msg_out = Message::new(); let mut file_action = FileAction::new(); @@ -136,4 +158,8 @@ pub trait FileManager: Interface { is_upload, ))); } + + fn rename_file(&self, act_id: i32, path: String, new_name: String, is_remote: bool) { + self.send(Data::RenameFile((act_id, path, new_name, is_remote))); + } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1280fb80120b..fb7cba3c51c3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + ffi::c_void, num::NonZeroI64, sync::{ atomic::{AtomicUsize, Ordering}, @@ -7,16 +8,26 @@ use std::{ }, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +#[cfg(not(any(target_os = "ios")))] +use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER}; +use crate::{ + client::{ + self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, + }, + common::get_default_sound_input, + ui_session_interface::{InvokeUiSession, Session}, +}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::sleep; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, - config::{PeerConfig, TransferSerde}, + config::{self, LocalConfig, PeerConfig, TransferSerde}, fs::{ self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, @@ -37,21 +48,8 @@ use hbb_common::{ use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; -use crate::client::{ - self, new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, -}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; -use crate::common::get_default_sound_input; -use crate::ui_session_interface::{InvokeUiSession, Session}; -#[cfg(not(any(target_os = "ios")))] -use crate::{audio_service, ConnInner, CLIENT_SERVER}; -use crate::{client::Data, client::Interface}; - pub struct Remote { handler: Session, - video_queue_map: Arc>>>, - video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, @@ -68,31 +66,38 @@ pub struct Remote { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: i32, // used for file clipboard data_count: Arc, - frame_count_map: Arc>>, video_format: CodecFormat, elevation_requested: bool, - fps_control: FpsControl, - decode_fps: Arc>>, + peer_info: ParsedPeerInfo, + video_threads: HashMap, chroma: Arc>>, + last_record_state: bool, +} + +#[derive(Default)] +struct ParsedPeerInfo { + platform: String, + is_installed: bool, + idd_impl: String, +} + +impl ParsedPeerInfo { + fn is_support_virtual_display(&self) -> bool { + self.is_installed + && self.platform == "Windows" + && (self.idd_impl == "rustdesk_idd" || self.idd_impl == "amyuni_idd") + } } impl Remote { pub fn new( handler: Session, - video_queue: Arc>>>, - video_sender: MediaSender, - audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, - frame_count_map: Arc>>, - decode_fps: Arc>>, - chroma: Arc>>, ) -> Self { Self { handler, - video_queue_map: video_queue, - video_sender, - audio_sender, + audio_sender: crate::client::start_audio_thread(), receiver, sender, read_jobs: Vec::new(), @@ -105,18 +110,40 @@ impl Remote { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), - frame_count_map, video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, elevation_requested: false, - fps_control: Default::default(), - decode_fps, - chroma, + peer_info: Default::default(), + video_threads: Default::default(), + chroma: Default::default(), + last_record_state: false, } } pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + let _file_clip_context_holder = { + // `is_port_forward()` will not reach here, but we still check it for clarity. + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + // 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() { @@ -173,8 +200,7 @@ impl Remote { crate::rustdesk_interval(time::interval(Duration::new(1, 0))); let mut fps_instant = Instant::now(); - let _keep_it = - client::hc_connection(feedback, rendezvous_server, token).await; + let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; loop { tokio::select! { @@ -235,7 +261,6 @@ impl Remote { } } _ = status_timer.tick() => { - self.fps_control(direct); let elapsed = fps_instant.elapsed().as_millis(); if elapsed < 1000 { continue; @@ -245,14 +270,14 @@ impl Remote { speed = speed * 1000 / elapsed as usize; let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let mut frame_count_map_write = self.frame_count_map.write().unwrap(); - let frame_count_map = frame_count_map_write.clone(); - frame_count_map_write.values_mut().for_each(|v| *v = 0); - drop(frame_count_map_write); - let fps = frame_count_map.iter().map(|(k, v)| { + let fps = self.video_threads.iter().map(|(k, v)| { // Correcting the inaccuracy of status_timer - (k.clone(), (*v as i32) * 1000 / elapsed as i32) + (k.clone(), (*v.frame_count.read().unwrap() as i32) * 1000 / elapsed as i32) }).collect::>(); + self.video_threads.iter().for_each(|(_, v)| { + *v.frame_count.write().unwrap() = 0; + }); + self.fps_control(direct, fps.clone()); let chroma = self.chroma.read().unwrap().clone(); let chroma = match chroma { Some(Chroma::I444) => "4:4:4", @@ -260,10 +285,16 @@ impl Remote { None => "-", }; let chroma = Some(chroma.to_string()); + let codec_format = if self.video_format == CodecFormat::Unknown { + None + } else { + Some(self.video_format.clone()) + }; self.handler.update_quality_status(QualityStatus { speed: Some(speed), fps, chroma, + codec_format, ..Default::default() }); } @@ -287,7 +318,7 @@ impl Remote { .unwrap() .set_disconnected(round); - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if _set_disconnected_ok { Client::try_stop_clipboard(); } @@ -338,6 +369,7 @@ impl Remote { } else { 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 }; log::debug!("Send system clipboard message to remote"); let msg = crate::clipboard_file::clip_2_msg(clip); @@ -482,6 +514,22 @@ impl Remote { self.check_clipboard_file_context(); } Data::Message(msg) => { + match &msg.union { + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::RefreshVideo(_)) => { + self.video_threads.iter().for_each(|(_, v)| { + *v.discard_queue.write().unwrap() = true; + }); + } + Some(misc::Union::RefreshVideoDisplay(display)) => { + if let Some(v) = self.video_threads.get_mut(&(display as usize)) { + *v.discard_queue.write().unwrap() = true; + } + } + _ => {} + }, + _ => {} + } allow_err!(peer.send(&msg).await); } Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { @@ -802,10 +850,28 @@ impl Remote { } } } - Data::RecordScreen(start, display, w, h, id) => { - let _ = self - .video_sender - .send(MediaData::RecordScreen(start, display, w, h, id)); + Data::RenameFile((id, path, new_name, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_rename(FileRename { + id, + path, + new_name, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + let err = fs::rename_file(&path, &new_name) + .err() + .map(|e| e.to_string()); + self.handle_job_status(id, -1, err); + } + } + Data::RecordScreen(start) => { + self.handler.lc.write().unwrap().record_state = start; + self.update_record_state(); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -848,9 +914,18 @@ impl Remote { .on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } - Data::ResetDecoder(display) => { - self.video_sender.send(MediaData::Reset(display)).ok(); - } + Data::ResetDecoder(display) => match display { + Some(display) => { + if let Some(v) = self.video_threads.get_mut(&display) { + v.video_sender.send(MediaData::Reset).ok(); + } + } + None => { + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::Reset).ok(); + } + } + }, _ => {} } true @@ -923,19 +998,24 @@ impl Remote { true } - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); + async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { + if !self.peer_info.is_support_virtual_display() { + return; + } + let lc = self.handler.lc.read().unwrap(); + let displays = lc.get_option("virtual-display"); + for d in displays.split(',') { + if let Ok(index) = d.parse::() { + let mut misc = Misc::new(); + misc.set_toggle_virtual_display(ToggleVirtualDisplay { + display: index, + on: true, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } } } @@ -945,6 +1025,11 @@ impl Remote { && lc.get_toggle_option("privacy-mode") { let impl_key = lc.get_option("privacy-mode-impl-key"); + if impl_key == crate::privacy_mode::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY + && !self.peer_info.is_support_virtual_display() + { + return; + } let mut misc = Misc::new(); misc.set_toggle_privacy_mode(TogglePrivacyMode { impl_key, @@ -968,100 +1053,115 @@ impl Remote { } } + // Currently, this function only considers decoding speed and queue length, not network delay. + // The controlled end can consider auto fps as the maximum decoding fps. #[inline] - fn fps_control(&mut self, direct: bool) { + fn fps_control(&mut self, direct: bool, real_fps_map: HashMap) { + self.video_threads.iter_mut().for_each(|(k, v)| { + let real_fps = real_fps_map.get(k).cloned().unwrap_or_default(); + if real_fps == 0 { + v.fps_control.inactive_counter += 1; + } else { + v.fps_control.inactive_counter = 0; + } + }); let custom_fps = self.handler.lc.read().unwrap().custom_fps.clone(); let custom_fps = custom_fps.lock().unwrap().clone(); let mut custom_fps = custom_fps.unwrap_or(30); if custom_fps < 5 || custom_fps > 120 { custom_fps = 30; } - let ctl = &mut self.fps_control; - let len = self - .video_queue_map - .read() - .unwrap() + let inactive_threshold = 15; + let max_queue_len = self + .video_threads .iter() - .map(|v| v.1.len()) + .map(|v| v.1.video_queue.read().unwrap().len()) .max() .unwrap_or_default(); - let decode_fps = self.decode_fps.read().unwrap().clone(); - let Some(mut decode_fps) = decode_fps else { + let min_decode_fps = self + .video_threads + .iter() + .filter(|v| v.1.fps_control.inactive_counter < inactive_threshold) + .map(|v| *v.1.decode_fps.read().unwrap()) + .min() + .flatten(); + let Some(min_decode_fps) = min_decode_fps else { return; }; - if cfg!(feature = "flutter") { - let active_displays = ctl - .last_active_time - .iter() - .filter(|t| t.1.elapsed().as_secs() < 5) - .count(); - if active_displays > 1 { - decode_fps = decode_fps / active_displays; - } - } let mut limited_fps = if direct { - decode_fps * 9 / 10 // 30 got 27 + min_decode_fps * 9 / 10 // 30 got 27 } else { - decode_fps * 4 / 5 // 30 got 24 + min_decode_fps * 4 / 5 // 30 got 24 }; if limited_fps > custom_fps { limited_fps = custom_fps; } let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone(); - let should_decrease = (len > 1 - && last_auto_fps.clone().unwrap_or(custom_fps as _) > limited_fps) - || len > std::cmp::max(1, limited_fps / 2); - - // increase judgement - if len <= 1 { - if ctl.idle_counter < usize::MAX { + let displays = self.video_threads.keys().cloned().collect::>(); + let mut fps_trending = |display: usize| { + let thread = self.video_threads.get_mut(&display)?; + let ctl = &mut thread.fps_control; + let len = thread.video_queue.read().unwrap().len(); + let decode_fps = thread.decode_fps.read().unwrap().clone()?; + let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _); + if ctl.inactive_counter > inactive_threshold { + return None; + } + if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) { + ctl.idle_counter = 0; + return Some(false); + } + if len <= 1 { ctl.idle_counter += 1; + if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps { + return Some(true); + } } - } else { - ctl.idle_counter = 0; - } - let mut should_increase = false; - if let Some(last_auto_fps) = last_auto_fps.clone() { - // ever set - if last_auto_fps + 3 <= limited_fps && ctl.idle_counter > 3 { - // limited_fps is 3 larger than last set, and idle time is more than 3 seconds - should_increase = true; + if len > 1 { + ctl.idle_counter = 0; } - } + None + }; + let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect(); + let should_decrease = trendings.iter().any(|v| *v == Some(false)); + let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true)); if last_auto_fps.is_none() || should_decrease || should_increase { // limited_fps to ensure decoding is faster than encoding let mut auto_fps = limited_fps; - if should_decrease && limited_fps < len { + if should_decrease && limited_fps < max_queue_len { auto_fps = limited_fps / 2; } if auto_fps < 1 { auto_fps = 1; } - let mut misc = Misc::new(); - misc.set_option(OptionMessage { - custom_fps: auto_fps as _, - ..Default::default() - }); - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); - log::info!("Set fps to {}", auto_fps); - ctl.last_queue_size = len; - self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + if Some(auto_fps) != last_auto_fps { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: auto_fps as _, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + log::info!("Set fps to {}", auto_fps); + self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + } } // send refresh - for (display, video_queue) in self.video_queue_map.read().unwrap().iter() { - let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2); + for (display, thread) in self.video_threads.iter_mut() { + let ctl = &mut thread.fps_control; + let video_queue = thread.video_queue.read().unwrap(); + let tolerable = std::cmp::min(min_decode_fps, video_queue.capacity() / 2); if ctl.refresh_times < 20 // enough && (video_queue.len() > tolerable - && (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10)) + && (ctl.refresh_times == 0 || ctl.last_refresh_instant.map(|t|t.elapsed().as_secs() > 10).unwrap_or(false))) { // Refresh causes client set_display, left frames cause flickering. - while let Some(_) = video_queue.pop() {} + drop(video_queue); self.handler.refresh_video(*display as _); log::info!("Refresh display {} to reduce delay", display); ctl.refresh_times += 1; - ctl.last_refresh_instant = Instant::now(); + ctl.last_refresh_instant = Some(Instant::now()); } } } @@ -1074,42 +1174,32 @@ impl Remote { self.first_frame = true; self.handler.close_success(); self.handler.adapt_size(); - self.send_opts_after_login(peer).await; + self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await; } - let incoming_format = CodecFormat::from(&vf); - if self.video_format != incoming_format { - self.video_format = incoming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incoming_format), - ..Default::default() - }) - }; + self.video_format = CodecFormat::from(&vf); let display = vf.display as usize; - let mut video_queue_write = self.video_queue_map.write().unwrap(); - if !video_queue_write.contains_key(&display) { - video_queue_write.insert( - display, - ArrayQueue::::new(crate::client::VIDEO_QUEUE_SIZE), - ); + if !self.video_threads.contains_key(&display) { + self.new_video_thread(display); } + let Some(thread) = self.video_threads.get_mut(&display) else { + return true; + }; if Self::contains_key_frame(&vf) { - if let Some(video_queue) = video_queue_write.get_mut(&display) { - while let Some(_) = video_queue.pop() {} - } - self.video_sender + thread + .video_sender .send(MediaData::VideoFrame(Box::new(vf))) .ok(); } else { - if let Some(video_queue) = video_queue_write.get_mut(&display) { - video_queue.force_push(vf); + let video_queue = thread.video_queue.read().unwrap(); + if video_queue.force_push(vf).is_some() { + drop(video_queue); + self.handler.refresh_video(display as _); + } else { + thread.video_sender.send(MediaData::VideoQueue).ok(); } - self.video_sender.send(MediaData::VideoQueue(display)).ok(); } - self.fps_control - .last_active_time - .insert(display, Instant::now()); } Some(message::Union::Hash(hash)) => { self.handler @@ -1118,16 +1208,24 @@ impl Remote { } Some(message::Union::LoginResponse(lr)) => match lr.union { Some(login_response::Union::Error(err)) => { + if err == client::REQUIRE_2FA { + self.handler.lc.write().unwrap().enable_trusted_devices = + lr.enable_trusted_devices; + } if !self.handler.handle_login_error(&err) { return false; } } Some(login_response::Union::PeerInfo(pi)) => { + let peer_version = pi.version.clone(); + let peer_platform = pi.platform.clone(); + self.set_peer_info(&pi); self.handler.handle_peer_info(pi); + #[cfg(not(feature = "flutter"))] self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1138,23 +1236,33 @@ impl Remote { }, )); // To make sure current text clipboard data is updated. - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Some(mut rx) = rx { timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = Client::get_current_clipboard_msg() { - let sender = self.sender.clone(); - let permission_config = self.handler.get_permission_config(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(CLIPBOARD_INTERVAL as f32 / 1_000.).await; - if permission_config.is_text_clipboard_required() { - sender.send(Data::Message(msg_out)).ok(); - } - }); + if self.handler.lc.read().unwrap().sync_init_clipboard.v { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + tokio::spawn(async move { + if permission_config.is_text_clipboard_required() { + sender.send(Data::Message(msg_out)).ok(); + } + }); + } } + // to-do: Android, is `sync_init_clipboard` really needed? + // https://github.com/rustdesk/rustdesk/discussions/9010 + + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + crate::flutter::update_text_clipboard_required(); // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1162,7 +1270,7 @@ impl Remote { crate::plugin::handle_listen_event( crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(), self.handler.get_id(), - ) + ); } if self.handler.is_file_transfer() { @@ -1185,8 +1293,8 @@ impl Remote { Some(message::Union::Clipboard(cb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, Some(crate::client::get_old_clipboard_text())); - #[cfg(any(target_os = "android", target_os = "ios"))] + update_clipboard(vec![cb], ClipboardSide::Client); + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -1197,6 +1305,16 @@ impl Remote { self.handler.clipboard(content); } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); + } + } + Some(message::Union::MultiClipboards(_mcb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -1205,6 +1323,9 @@ impl Remote { } Some(message::Union::FileResponse(fr)) => { match fr.union { + Some(file_response::Union::EmptyDirs(res)) => { + self.handler.update_empty_dirs(res); + } Some(file_response::Union::Dir(fd)) => { #[cfg(windows)] let entries = fd.entries.to_vec(); @@ -1359,17 +1480,17 @@ impl Remote { // https://github.com/rustdesk/rustdesk/issues/3703#issuecomment-1474734754 match p.permission.enum_value() { Ok(Permission::Keyboard) => { + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); - *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); - *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } Ok(Permission::Audio) => { @@ -1387,6 +1508,8 @@ impl Remote { 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) => { @@ -1397,9 +1520,10 @@ impl Remote { } Some(misc::Union::SwitchDisplay(s)) => { self.handler.handle_peer_switch_display(&s); - self.video_sender - .send(MediaData::Reset(Some(s.display as _))) - .ok(); + if let Some(thread) = self.video_threads.get_mut(&(s.display as usize)) { + thread.video_sender.send(MediaData::Reset).ok(); + } + if s.width > 0 && s.height > 0 { self.handler.set_display( s.x, @@ -1558,7 +1682,7 @@ impl Remote { }, Some(message::Union::MessageBox(msgbox)) => { let mut link = msgbox.link; - if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { + if let Some(v) = config::HELPER_URL.get(&link as &str) { link = v.to_string(); } else { log::warn!("Message box ignore link {} for security", &link); @@ -1605,6 +1729,25 @@ impl Remote { true } + fn set_peer_info(&mut self, pi: &PeerInfo) { + self.peer_info.platform = pi.platform.clone(); + if let Ok(platform_additions) = + serde_json::from_str::>(&pi.platform_additions) + { + self.peer_info.is_installed = platform_additions + .get("is_installed") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); + self.peer_info.idd_impl = platform_additions + .get("idd_impl") + .map(|v| v.as_str()) + .flatten() + .unwrap_or_default() + .to_string(); + } + } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { match notification.union { Some(back_notification::Union::BlockInputState(state)) => { @@ -1779,6 +1922,7 @@ impl Remote { true } + #[cfg(not(feature = "flutter"))] fn check_clipboard_file_context(&self) { #[cfg(any( target_os = "windows", @@ -1811,7 +1955,7 @@ impl Remote { return; }; - let is_stopping_allowed = clip.is_stopping_allowed_from_peer(); + let is_stopping_allowed = clip.is_beginning_message(); let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( @@ -1828,6 +1972,77 @@ impl Remote { }); } } + + fn new_video_thread(&mut self, display: usize) { + let video_queue = Arc::new(RwLock::new(ArrayQueue::new(client::VIDEO_QUEUE_SIZE))); + let (video_sender, video_receiver) = std::sync::mpsc::channel::(); + let decode_fps = Arc::new(RwLock::new(None)); + let frame_count = Arc::new(RwLock::new(0)); + let discard_queue = Arc::new(RwLock::new(false)); + let video_thread = VideoThread { + video_queue: video_queue.clone(), + video_sender, + decode_fps: decode_fps.clone(), + frame_count: frame_count.clone(), + fps_control: Default::default(), + discard_queue: discard_queue.clone(), + }; + let handler = self.handler.ui_handler.clone(); + crate::client::start_video_thread( + self.handler.clone(), + display, + video_receiver, + video_queue, + decode_fps, + self.chroma.clone(), + discard_queue, + move |display: usize, + data: &mut scrap::ImageRgb, + _texture: *mut c_void, + pixelbuffer: bool| { + *frame_count.write().unwrap() += 1; + if pixelbuffer { + handler.on_rgba(display, data); + } else { + #[cfg(all(feature = "vram", feature = "flutter"))] + handler.on_texture(display, _texture); + } + }, + ); + self.video_threads.insert(display, video_thread); + 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(); + } } struct RemoveJob { @@ -1860,22 +2075,26 @@ impl RemoveJob { } } +#[derive(Debug, Default)] struct FpsControl { - last_queue_size: usize, refresh_times: usize, - last_refresh_instant: Instant, + last_refresh_instant: Option, idle_counter: usize, - last_active_time: HashMap, + inactive_counter: usize, } -impl Default for FpsControl { - fn default() -> Self { - Self { - last_queue_size: Default::default(), - refresh_times: Default::default(), - last_refresh_instant: Instant::now(), - idle_counter: 0, - last_active_time: Default::default(), - } +struct VideoThread { + video_queue: Arc>>, + video_sender: MediaSender, + decode_fps: Arc>>, + frame_count: Arc>, + 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/clipboard.rs b/src/clipboard.rs index 0db9f59c1440..ac3a83f00f72 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,26 +1,52 @@ -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Arc, Mutex, -}; - -use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; -use hbb_common::{ - allow_err, - compress::{compress as compress_func, decompress}, - log, - message_proto::*, - ResultType, +#[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, + time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; -const FAKE_SVG_WIDTH: usize = 999999; +// This format is used to store the flag in the clipboard. +const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; + +// Add special format for Excel XML Spreadsheet +const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; + +#[cfg(not(target_os = "android"))] lazy_static::lazy_static! { - pub static ref CONTENT: Arc> = Default::default(); static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); + // cache the clipboard msg + static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); + // For updating in server and getting content in cm. + // Clipboard on Linux is "server--clients" mode. + // The clipboard content is owned by the server and passed to the clients when requested. + // Plain text is the only exception, it does not require the server to be present. + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } +#[cfg(not(target_os = "android"))] +const CLIPBOARD_GET_MAX_RETRY: usize = 3; +#[cfg(not(target_os = "android"))] +const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); + +#[cfg(not(target_os = "android"))] +const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ + ClipboardFormat::Text, + ClipboardFormat::Html, + ClipboardFormat::Rtf, + ClipboardFormat::ImageRgba, + ClipboardFormat::ImagePng, + ClipboardFormat::ImageSvg, + 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(); @@ -61,7 +87,7 @@ fn parse_plain_uri_list(v: Vec) -> Result { #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] impl ClipboardContext { - pub fn new(_listen: bool) -> Result { + pub fn new() -> Result { let clipboard = get_clipboard()?; let string_getter = clipboard .getter @@ -126,202 +152,104 @@ impl ClipboardContext { } } +#[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, - old: Option>>, + side: ClipboardSide, + force: bool, ) -> Option { if ctx.is_none() { - *ctx = ClipboardContext::new(true).ok(); + *ctx = ClipboardContext::new().ok(); } let ctx2 = ctx.as_mut()?; - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - let content = ctx2.get(); - if let Ok(content) = content { - if !content.is_empty() { - if matches!(content, ClipboardData::Text(_)) { - // Skip the text if the last content is image-svg/html - if ctx2.is_last_plain { - return None; - } - } - - let changed = content != *old.lock().unwrap(); - if changed { - log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let msg = content.create_msg(); - *old.lock().unwrap() = content; + match ctx2.get(side, force) { + Ok(content) => { + if !content.is_empty() { + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; return Some(msg); } } + Err(e) => { + log::error!("Failed to get clipboard content. {}", e); + } } None } -fn update_clipboard_(clipboard: Clipboard, old: Option>>) { - let content = ClipboardData::from_msg(clipboard); - if content.is_empty() { - return; - } - match ClipboardContext::new(false) { - Ok(mut ctx) => { - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - allow_err!(ctx.set(&content)); - *old.lock().unwrap() = content; - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); - } - Err(err) => { - log::error!("Failed to create clipboard context: {}", err); +#[cfg(target_os = "windows")] +pub fn check_clipboard_cm() -> ResultType { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + hbb_common::bail!("Failed to create clipboard context: {}", e); + } } } -} - -pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { - std::thread::spawn(move || { - update_clipboard_(clipboard, old); - }); -} - -#[derive(Clone)] -pub enum ClipboardData { - Text(String), - Image(arboard::ImageData<'static>, u64), - Empty, -} - -impl Default for ClipboardData { - fn default() -> Self { - ClipboardData::Empty + if let Some(ctx) = ctx.as_mut() { + let content = ctx.get(ClipboardSide::Host, false)?; + let clipboards = proto::create_multi_clipboards(content); + Ok(clipboards) + } else { + hbb_common::bail!("Failed to create clipboard context"); } } -impl ClipboardData { - fn image(image: arboard::ImageData<'static>) -> ClipboardData { - let hash = 0; - /* - use std::hash::{DefaultHasher, Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - image.bytes.hash(&mut hasher); - let hash = hasher.finish(); - */ - ClipboardData::Image(image, hash) - } - - pub fn is_empty(&self) -> bool { - match self { - ClipboardData::Empty => true, - ClipboardData::Text(s) => s.is_empty(), - ClipboardData::Image(a, _) => a.bytes().is_empty(), - } +#[cfg(not(target_os = "android"))] +fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { + let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + if to_update_data.is_empty() { + return; } - - fn from_msg(clipboard: Clipboard) -> Self { - let is_image = clipboard.width > 0; - let data = if clipboard.compress { - decompress(&clipboard.content) - } else { - clipboard.content.into() - }; - if is_image { - // We cannot use data.start_with(b" { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; } } } - - pub fn create_msg(&self) -> Message { - let mut msg = Message::new(); - - match self { - ClipboardData::Text(s) => { - let compressed = compress_func(s.as_bytes()); - let compress = compressed.len() < s.as_bytes().len(); - let content = if compress { - compressed - } else { - s.clone().into_bytes() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - ..Default::default() - }); - } - ClipboardData::Image(a, _) => { - let compressed = compress_func(&a.bytes()); - let compress = compressed.len() < a.bytes().len(); - let content = if compress { - compressed - } else { - a.bytes().to_vec() - }; - let (w, h) = match a { - arboard::ImageData::Rgba(a) => (a.width, a.height), - arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _), - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - width: w as _, - height: h as _, - ..Default::default() - }); - } - _ => {} + if let Some(ctx) = ctx.as_mut() { + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); + } else { + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } - msg } } -impl PartialEq for ClipboardData { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, - (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) { - (arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => { - a.width == b.width && a.height == b.height && a.bytes == b.bytes - } - (arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b, - _ => false, - }, - (ClipboardData::Empty, ClipboardData::Empty) => true, - _ => false, - } - } +#[cfg(not(target_os = "android"))] +pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { + std::thread::spawn(move || { + update_clipboard_(multi_clipboards, side); + }); } +#[cfg(not(target_os = "android"))] #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, - counter: (Arc, u64), - shutdown: Option, - is_last_plain: bool, } +#[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(listen: bool) -> ResultType { + pub fn new() -> ResultType { let board; #[cfg(not(target_os = "linux"))] { @@ -351,94 +279,395 @@ impl ClipboardContext { } } - // starting from 1 so that we can always get initial clipboard data no matter if change - let change_count: Arc = Arc::new(AtomicU64::new(1)); - let mut shutdown = None; - if listen { - struct Handler(Arc); - impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - self.0.fetch_add(1, Ordering::SeqCst); - CallbackResult::Next - } + Ok(ClipboardContext { inner: board }) + } - fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { - log::trace!("Error of clipboard listener: {}", error); - CallbackResult::Next + fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType> { + // If there're multiple threads or processes trying to access the clipboard at the same time, + // the previous clipboard owner will fail to access the clipboard. + // `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time. + // See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34 + // + // This is a common case on Windows, so we retry here. + // Related issues: + // 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) { + Ok(data) => { + return Ok(data + .into_iter() + .filter(|c| !matches!(c, arboard::ClipboardData::None)) + .collect()) } + Err(e) => match e { + arboard::Error::ClipboardOccupied => { + log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1); + std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR); + } + _ => { + log::error!("Failed to get clipboard formats, {}", e); + return Err(e.into()); + } + }, } - let change_count_cloned = change_count.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. - std::thread::spawn(move || match Master::new(Handler(change_count_cloned)) { - Ok(mut master) => { - tx.send(master.shutdown_channel()).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"); + } + bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed"); + } + + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let _lock = ARBOARD_MTX.lock().unwrap(); + let data = self.get_formats(SUPPORTED_FORMATS)?; + if data.is_empty() { + return Ok(data); + } + if !force { + for c in data.iter() { + if let ClipboardData::Special((s, d)) = c { + if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) { + return Ok(vec![]); } } - Err(err) => { - log::error!("Failed to create clipboard listener: {}", err); + } + } + Ok(data + .into_iter() + .filter(|c| match c { + ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + _ => true, + }) + .collect()) + } + + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { + let _lock = ARBOARD_MTX.lock().unwrap(); + self.inner.set_formats(data)?; + Ok(()) + } +} + +pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { + use hbb_common::get_version_number; + if get_version_number(peer_version) < get_version_number("1.3.0") { + return false; + } + if ["", &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") + { + return false; + } + true +} + +#[cfg(not(target_os = "android"))] +pub fn get_current_clipboard_msg( + peer_version: &str, + peer_platform: &str, + side: ClipboardSide, +) -> Option { + let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap(); + if multi_clipboards.clipboards.is_empty() { + let mut ctx = ClipboardContext::new().ok()?; + *multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?); + } + if multi_clipboards.clipboards.is_empty() { + return None; + } + + if is_support_multi_clipboard(peer_version, peer_platform) { + let mut msg = Message::new(); + msg.set_multi_clipboards(multi_clipboards.clone()); + Some(msg) + } else { + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSide { + Host, + Client, +} + +impl ClipboardSide { + // 01: the clipboard is owned by the host + // 10: the clipboard is owned by the client + fn get_owner_data(&self) -> Vec { + match self { + ClipboardSide::Host => vec![0b01], + ClipboardSide::Client => vec![0b10], + } + } + + fn is_owner(&self, data: &[u8]) -> bool { + if data.len() == 0 { + return false; + } + data[0] & 0b11 != 0 + } +} + +impl std::fmt::Display for ClipboardSide { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardSide::Host => write!(f, "host"), + ClipboardSide::Client => write!(f, "client"), + } + } +} + +#[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"))] + use arboard::ClipboardData; + use hbb_common::{ + compress::{compress as compress_func, decompress}, + message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, + }; + + fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: format.into(), + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn image_to_proto(a: arboard::ImageData) -> Clipboard { + match &a { + arboard::ImageData::Rgba(rgba) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + width: rgba.width as _, + height: rgba.height as _, + format: ClipboardFormat::ImageRgba.into(), + ..Default::default() + } + } + arboard::ImageData::Png(png) => Clipboard { + compress: false, + content: png.to_owned().to_vec().into(), + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }, + arboard::ImageData::Svg(_) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::ImageSvg.into(), + ..Default::default() } - }); - if let Ok(st) = rx.recv() { - shutdown = Some(st); } } - Ok(ClipboardContext { - inner: board, - counter: (change_count, 0), - shutdown, - is_last_plain: false, - }) } - #[inline] - pub fn change_count(&self) -> u64 { - debug_assert!(self.shutdown.is_some()); - self.counter.0.load(Ordering::SeqCst) + fn special_to_proto(d: Vec, s: String) -> Clipboard { + let compressed = compress_func(&d); + let compress = compressed.len() < d.len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::Special.into(), + special_name: s, + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn clipboard_data_to_proto(data: ClipboardData) -> Option { + let d = match data { + ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), + ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), + ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), + ClipboardData::Image(a) => image_to_proto(a), + ClipboardData::Special((s, d)) => special_to_proto(d, s), + _ => return None, + }; + Some(d) } - pub fn get(&mut self) -> ResultType { - let cn = self.change_count(); - let _lock = ARBOARD_MTX.lock().unwrap(); - // only for image for the time being, - // because I do not want to change behavior of text clipboard for the time being - if cn != self.counter.1 { - self.is_last_plain = false; - self.counter.1 = cn; - if let Ok(image) = self.inner.get_image() { - // Both text and image svg may be set by some applications - // But we only want to send the svg content. - // - // We can't call `get_text()` and store current text in `old` in outer scope, - // because it may be updated later than svg. - // Then the text will still be sent and replace the image svg content. - self.is_last_plain = matches!(image, arboard::ImageData::Svg(_)); - return Ok(ClipboardData::image(image)); + #[cfg(not(target_os = "android"))] + pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { + MultiClipboards { + clipboards: vec_data + .into_iter() + .filter_map(clipboard_data_to_proto) + .collect(), + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn from_clipboard(clipboard: Clipboard) -> Option { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + match clipboard.format.enum_value() { + Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text), + Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf), + Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html), + Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba( + clipboard.width as _, + clipboard.height as _, + data.into(), + ))), + Ok(ClipboardFormat::ImagePng) => { + Some(ClipboardData::Image(arboard::ImageData::png(data.into()))) } + Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( + std::str::from_utf8(&data).unwrap_or_default(), + ))), + Ok(ClipboardFormat::Special) => { + Some(ClipboardData::Special((clipboard.special_name, data))) + } + _ => None, } - Ok(ClipboardData::Text(self.inner.get_text()?)) } - fn set(&mut self, data: &ClipboardData) -> ResultType<()> { - let _lock = ARBOARD_MTX.lock().unwrap(); - match data { - ClipboardData::Text(s) => self.inner.set_text(s)?, - ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?, - _ => {} + #[cfg(not(target_os = "android"))] + pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { + multi_clipboards + .into_iter() + .filter_map(from_clipboard) + .collect() + } + + pub fn get_msg_if_not_support_multi_clip( + version: &str, + platform: &str, + multi_clipboards: &MultiClipboards, + ) -> Option { + if crate::clipboard::is_support_multi_clipboard(version, platform) { + return None; } - Ok(()) + + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_clipboard(mut cb: Clipboard) { + use hbb_common::protobuf::Message; + + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + let multi_clips = MultiClipboards { + clipboards: vec![cb], + ..Default::default() + }; + if let Ok(bytes) = multi_clips.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) { + use hbb_common::protobuf::Message; + + for cb in mcb.clipboards.iter_mut() { + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + } + if let Ok(bytes) = mcb.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); } } -impl Drop for ClipboardContext { - fn drop(&mut self) { - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.signal(); +#[cfg(target_os = "android")] +pub fn get_clipboards_msg(client: bool) -> Option { + let mut clipboards = scrap::android::ffi::get_clipboards(client)?; + let mut msg = Message::new(); + for c in &mut clipboards.clipboards { + let compressed = hbb_common::compress::compress(&c.content); + let compress = compressed.len() < c.content.len(); + if compress { + c.content = compressed.into(); } + c.compress = compress; } + msg.set_multi_clipboards(clipboards); + Some(msg) } diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index a4bfc1aef699..4548cdbea1ae 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,7 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { requested_data: data.requested_data.into(), }) } + Some(cliprdr::Union::TryEmpty(_)) => Some(ClipboardFile::TryEmpty), _ => None, } } diff --git a/src/common.rs b/src/common.rs index 740b5d9e375a..14db9b5d8955 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,7 +5,7 @@ use std::{ task::Poll, }; -use serde_json::Value; +use serde_json::{json, Map, Value}; use hbb_common::{ allow_err, @@ -84,6 +84,7 @@ lazy_static::lazy_static! { // Is server logic running. The server code can invoked to run by the main process if --server is not running. static ref SERVER_RUNNING: Arc> = Default::default(); static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); + static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned()); } pub struct SimpleCallOnReturn { @@ -137,6 +138,11 @@ pub fn is_main() -> bool { *IS_MAIN } +#[inline] +pub fn is_cm() -> bool { + *IS_CM +} + // Is server logic running. #[inline] pub fn is_server_running() -> bool { @@ -810,18 +816,28 @@ pub fn 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(); + 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")] + { + let mut m = HashMap::new(); + m.insert("name", "check_software_update_finish"); + m.insert("url", &response_url); + if let Ok(data) = serde_json::to_string(&m) { + let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + } + } *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } Ok(()) @@ -1036,6 +1052,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec) -> String { + let fd_json = _make_fd_to_json(id, path, entries); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +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)); @@ -1051,7 +1072,33 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin entries_out.push(entry_map); } fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) + fd_json +} + +pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String { + let mut fd_jsons = vec![]; + + for fd in fds.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + + serde_json::to_string(&fd_jsons).unwrap_or("".into()) +} + +pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String { + let mut map: Map = serde_json::Map::new(); + map.insert("path".into(), json!(res.path)); + + let mut fd_jsons = vec![]; + + for fd in res.empty_dirs.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + map.insert("empty_dirs".into(), fd_jsons.into()); + + serde_json::to_string(&map).unwrap_or("".into()) } /// The function to handle the url scheme sent by the system. @@ -1359,7 +1406,7 @@ fn read_custom_client_advanced_settings( } else { config::DEFAULT_SETTINGS.write().unwrap() }; - let mut buildin_settings = config::BUILDIN_SETTINGS.write().unwrap(); + let mut buildin_settings = config::BUILTIN_SETTINGS.write().unwrap(); if let Some(settings) = settings.as_object() { for (k, v) in settings { @@ -1494,6 +1541,15 @@ pub fn is_empty_uni_link(arg: &str) -> bool { arg[prefix.len()..].chars().all(|c| c == '/') } +pub fn get_hwid() -> Bytes { + use hbb_common::sha2::{Digest, Sha256}; + + let uuid = hbb_common::get_uuid(); + let mut hasher = Sha256::new(); + hasher.update(&uuid); + Bytes::from(hasher.finalize().to_vec()) +} + #[cfg(test)] mod tests { use super::*; @@ -1635,3 +1691,13 @@ 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 bf7e536f6b27..5264d5bfd1e2 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -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; @@ -39,6 +42,7 @@ pub fn core_main() -> Option> { let mut _is_run_as_system = false; let mut _is_quick_support = false; let mut _is_flutter_invoke_new_connection = false; + let mut no_server = false; let mut arg_exe = Default::default(); for arg in std::env::args() { if i == 0 { @@ -62,6 +66,8 @@ pub fn core_main() -> Option> { _is_run_as_system = true; } else if arg == "--quick_support" { _is_quick_support = true; + } else if arg == "--no-server" { + no_server = true; } else { args.push(arg); } @@ -134,6 +140,7 @@ pub fn core_main() -> Option> { } } hbb_common::init_log(false, &log_name); + log::info!("main start args: {:?}, env: {:?}", args, std::env::args()); // linux uni (url) go here. #[cfg(all(target_os = "linux", feature = "flutter"))] @@ -161,9 +168,8 @@ pub fn core_main() -> Option> { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); - log::info!("main start args:{:?}", args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { - std::thread::spawn(move || crate::start_server(false)); + std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] { @@ -261,15 +267,29 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); + #[cfg(target_os = "linux")] + { + hbb_common::allow_err!(crate::platform::check_autostart_config()); + std::process::Command::new("pkill") + .arg("-f") + .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)>, + )); + } #[cfg(windows)] crate::privacy_mode::restore_reg_connectivity(true); #[cfg(any(target_os = "linux", target_os = "windows"))] { - crate::start_server(true); + crate::start_server(true, false); } #[cfg(target_os = "macos")] { - let handler = std::thread::spawn(move || crate::start_server(true)); + let handler = std::thread::spawn(move || crate::start_server(true, false)); crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); @@ -302,6 +322,20 @@ pub fn core_main() -> Option> { } } return None; + } else if args[0] == "--set-unlock-pin" { + #[cfg(feature = "flutter")] + if args.len() == 2 { + if crate::platform::is_installed() && is_root() { + if let Err(err) = crate::ipc::set_unlock_pin(args[1].to_owned(), false) { + println!("{err}"); + } else { + println!("Done!"); + } + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; } else if args[0] == "--get-id" { println!("{}", crate::ipc::get_id()); return None; @@ -377,13 +411,32 @@ pub fn core_main() -> Option> { if pos < max { strategy_name = Some(args[pos + 1].to_owned()); } + let mut address_book_name = None; + let pos = args + .iter() + .position(|x| x == "--address_book_name") + .unwrap_or(max); + if pos < max { + address_book_name = Some(args[pos + 1].to_owned()); + } + let mut address_book_tag = None; + let pos = args + .iter() + .position(|x| x == "--address_book_tag") + .unwrap_or(max); + if pos < max { + address_book_tag = 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() { - println!("--user_name or --strategy_name is required!"); + if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() + { + println!( + "--user_name or --strategy_name or --address_book_name is required!" + ); } else { if let Some(name) = user_name { body["user_name"] = serde_json::json!(name); @@ -391,6 +444,12 @@ pub fn core_main() -> Option> { if let Some(name) = strategy_name { body["strategy_name"] = serde_json::json!(name); } + if let Some(name) = address_book_name { + body["address_book_name"] = serde_json::json!(name); + if let Some(name) = address_book_tag { + body["address_book_tag"] = 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) { Err(err) => println!("{}", err), @@ -420,8 +479,18 @@ pub fn core_main() -> Option> { crate::ui_interface::start_option_status_sync(); } else if args[0] == "--cm-no-ui" { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))] - crate::flutter::connection_manager::start_cm_no_ui(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::ui_interface::start_option_status_sync(); + crate::flutter::connection_manager::start_cm_no_ui(); + } + return None; + } else if args[0] == "-gtk-sudo" { + // rustdesk service kill `rustdesk --` processes + #[cfg(target_os = "linux")] + if args.len() > 2 { + crate::platform::gtk_sudo::exec(); + } return None; } else { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/custom_server.rs b/src/custom_server.rs index 58d34d853884..18118788e323 100644 --- a/src/custom_server.rs +++ b/src/custom_server.rs @@ -56,33 +56,34 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType { * * This allows using a ',' (comma) symbol as a final delimiter. */ - if s.contains("host=") { - let stripped = &s[s.find("host=").unwrap_or(0)..s.len()]; + if s.to_lowercase().contains("host=") { + let stripped = &s[s.to_lowercase().find("host=").unwrap_or(0)..s.len()]; let strs: Vec<&str> = stripped.split(",").collect(); - let mut host = ""; - let mut key = ""; - let mut api = ""; - let mut relay = ""; + let mut host = String::default(); + let mut key = String::default(); + let mut api = String::default(); + let mut relay = String::default(); let strs_iter = strs.iter(); for el in strs_iter { - if el.starts_with("host=") { - host = &el[5..el.len()]; + let el_lower = el.to_lowercase(); + if el_lower.starts_with("host=") { + host = el.chars().skip(5).collect(); } - if el.starts_with("key=") { - key = &el[4..el.len()]; + if el_lower.starts_with("key=") { + key = el.chars().skip(4).collect(); } - if el.starts_with("api=") { - api = &el[4..el.len()]; + if el_lower.starts_with("api=") { + api = el.chars().skip(4).collect(); } - if el.starts_with("relay=") { - relay = &el[4..el.len()]; + if el_lower.starts_with("relay=") { + relay = el.chars().skip(6).collect(); } } return Ok(CustomServer { - host: host.to_owned(), - key: key.to_owned(), - api: api.to_owned(), - relay: relay.to_owned(), + host, + key, + api, + relay, }); } else { let s = s @@ -146,8 +147,10 @@ mod test { } ); assert_eq!( - get_custom_server_from_string("rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,.exe") - .unwrap(), + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,.exe" + ) + .unwrap(), CustomServer { host: "server.example.net".to_owned(), key: "Zm9vYmFyLiwyCg==".to_owned(), @@ -155,6 +158,30 @@ mod test { relay: "".to_owned(), } ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,relay=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-Host=server.example.net,Key=Zm9vYmFyLiwyCg==,RELAY=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); let lic = CustomServer { host: "1.1.1.1".to_owned(), key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(), diff --git a/src/flutter.rs b/src/flutter.rs index cd6e51ea1b00..255a00e0fd5e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -17,8 +17,9 @@ use serde::Serialize; use serde_json::json; use std::{ - collections::HashMap, + 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. @@ -726,6 +757,20 @@ impl InvokeUiSession for FlutterHandler { } } + fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) { + self.push_event( + "empty_dirs", + &[ + ("is_local", "false"), + ( + "value", + &crate::common::make_empty_dirs_response_to_json(&res), + ), + ], + &[], + ); + } + // unused in flutter fn update_transfer_list(&self) {} @@ -802,13 +847,13 @@ impl InvokeUiSession for FlutterHandler { fn set_peer_info(&self, pi: &PeerInfo) { let displays = Self::make_displays_msg(&pi.displays); - let mut features: HashMap<&str, i32> = Default::default(); + let mut features: HashMap<&str, bool> = Default::default(); for ref f in pi.features.iter() { - features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); + features.insert("privacy_mode", f.privacy_mode); } // compatible with 1.1.9 if get_version_number(&pi.version) < get_version_number("1.2.0") { - features.insert("privacy_mode", 0); + features.insert("privacy_mode", false); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); let resolutions = serialize_resolutions(&pi.resolutions.resolutions); @@ -1010,6 +1055,10 @@ impl InvokeUiSession for FlutterHandler { rgba_data.valid = false; } } + + fn update_record_status(&self, start: bool) { + self.push_event("record_status", &[("start", &start.to_string())], &[]); + } } impl FlutterHandler { @@ -1122,6 +1171,7 @@ pub fn session_add( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1176,6 +1226,7 @@ pub fn session_add( force_relay, get_adapter_luid(), shared_password, + conn_token, ); let session = Arc::new(session.clone()); @@ -1244,18 +1295,33 @@ fn try_send_close_event(event_stream: &Option>) { } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn update_text_clipboard_required() { let is_required = sessions::get_sessions() .iter() .any(|s| s.is_text_clipboard_required()); + #[cfg(target_os = "android")] + let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); Client::set_is_text_clipboard_required(is_required); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { + // Check if the client supports multi clipboards + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + let version = s.ui_handler.peer_info.read().unwrap().version.clone(); + let platform = s.ui_handler.peer_info.read().unwrap().platform.clone(); + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &version, + &platform, + multi_clipboards, + ) { + s.send(Data::Message(msg_out)); + continue; + } + } s.send(Data::Message(msg.clone())); } } @@ -1767,9 +1833,56 @@ pub fn try_sync_peer_option( } } +pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i32, on: bool) { + let virtual_display_key = "virtual-display"; + let displays = session.get_option(virtual_display_key.to_owned()); + if !on { + if index == -1 { + if !displays.is_empty() { + session.set_option(virtual_display_key.to_owned(), "".to_owned()); + } + } else { + let mut vdisplays = displays.split(',').collect::>(); + let len = vdisplays.len(); + if index == 0 { + // 0 means we cann't toggle the virtual display by index. + vdisplays.remove(vdisplays.len() - 1); + } else { + if let Some(i) = vdisplays.iter().position(|&x| x == index.to_string()) { + vdisplays.remove(i); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } + } else { + let mut vdisplays = displays + .split(',') + .map(|x| x.to_string()) + .collect::>(); + let len = vdisplays.len(); + if index == 0 { + vdisplays.push(index.to_string()); + } else { + if !vdisplays.iter().any(|x| *x == index.to_string()) { + vdisplays.push(index.to_string()); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } +} + // sessions mod is used to avoid the big lock of sessions' map. pub mod sessions { - use std::collections::HashSet; use super::*; @@ -1985,7 +2098,7 @@ pub mod sessions { } #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn has_sessions_running(conn_type: ConnType) -> bool { SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| { *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 @@ -1994,20 +2107,16 @@ pub mod sessions { } pub(super) mod async_tasks { - use hbb_common::{ - bail, - tokio::{ - self, select, - sync::mpsc::{unbounded_channel, UnboundedSender}, - }, - ResultType, - }; + use hbb_common::{bail, tokio, ResultType}; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{ + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, }; - type TxQueryOnlines = UnboundedSender>; + type TxQueryOnlines = SyncSender>; lazy_static::lazy_static! { static ref TX_QUERY_ONLINES: Arc>> = Default::default(); } @@ -2024,21 +2133,18 @@ pub(super) mod async_tasks { #[tokio::main(flavor = "current_thread")] async fn start_flutter_async_runner_() { - let (tx_onlines, mut rx_onlines) = unbounded_channel::>(); + // Only one task is allowed to run at the same time. + let (tx_onlines, rx_onlines) = sync_channel::>(1); TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines); loop { - select! { - ids = rx_onlines.recv() => { - match ids { - Some(_ids) => { - #[cfg(not(any(target_os = "ios")))] - crate::rendezvous_mediator::query_online_states(_ids, handle_query_onlines).await - } - None => { - break; - } - } + match rx_onlines.recv() { + Ok(ids) => { + crate::client::peer_online::query_online_states(ids, handle_query_onlines).await + } + _ => { + // unreachable! + break; } } } @@ -2046,7 +2152,8 @@ pub(super) mod async_tasks { pub fn query_onlines(ids: Vec) -> ResultType<()> { if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() { - let _ = tx.send(ids)?; + // Ignore if the channel is full. + let _ = tx.try_send(ids)?; } else { bail!("No tx_query_onlines"); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 08e79cbea94b..9e23b7b02674 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,17 +1,14 @@ +#[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::{is_keyboard_mode_supported, make_fd_to_json}, + common::{make_fd_to_json, make_vec_fd_to_json}, flutter::{ self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, }, 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")))] @@ -19,13 +16,11 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, - message_proto::KeyboardMode, rendezvous_proto::ConnType, ResultType, }; use std::{ collections::HashMap, - str::FromStr, sync::{ atomic::{AtomicI32, Ordering}, Arc, @@ -63,7 +58,6 @@ fn initialize(app_dir: &str, custom_client_config: &str) { scrap::mediacodec::check_mediacodec(); crate::common::test_rendezvous_server(); crate::common::test_nat_type(); - crate::common::check_software_update(); } #[cfg(target_os = "ios")] { @@ -124,6 +118,7 @@ pub fn session_add_sync( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> SyncReturn { if let Err(e) = session_add( &session_id, @@ -135,6 +130,7 @@ pub fn session_add_sync( force_relay, password, is_shared_password, + conn_token, ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -208,14 +204,27 @@ pub fn session_login( } } -pub fn session_send2fa(session_id: SessionID, code: String) { +pub fn session_send2fa(session_id: SessionID, code: String, trust_this_device: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.send2fa(code); + session.send2fa(code, trust_this_device); } } +pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn { + let v = if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.get_enable_trusted_devices() + } else { + false + }; + SyncReturn(v) +} + pub fn session_close(session_id: SessionID) { if let Some(session) = sessions::remove_session_by_session_id(&session_id) { + // `release_remote_keys` is not required for mobile platforms in common cases. + // But we still call it to make the code more stable. + #[cfg(any(target_os = "android", target_os = "ios"))] + crate::keyboard::release_remote_keys("map"); session.close_event_stream(session_id); session.close(); } @@ -235,21 +244,17 @@ pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { } } -pub fn session_record_screen( - session_id: SessionID, - start: bool, - display: usize, - width: usize, - height: usize, -) { +pub fn session_record_screen(session_id: SessionID, start: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_screen(start, display as _, width as _, height as _); + session.record_screen(start); } } -pub fn session_record_status(session_id: SessionID, status: bool) { +pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_status(status); + SyncReturn(session.is_recording()) + } else { + SyncReturn(false) } } @@ -266,7 +271,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { session.toggle_option(value.clone()); try_sync_peer_option(&session, &session_id, &value, None); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } @@ -438,15 +443,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { - SyncReturn(is_keyboard_mode_supported( - &mode, - session.get_peer_version(), - &session.peer_platform(), - )) - } else { - SyncReturn(false) - } + SyncReturn(session.is_keyboard_mode_supported(mode)) } else { SyncReturn(false) } @@ -481,6 +478,25 @@ pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Ve } pub fn session_handle_flutter_key_event( + session_id: SessionID, + character: String, + usb_hid: i32, + lock_modes: i32, + down_or_up: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let keyboard_mode = session.get_keyboard_mode(); + session.handle_flutter_key_event( + &keyboard_mode, + &character, + usb_hid, + lock_modes, + down_or_up, + ); + } +} + +pub fn session_handle_flutter_raw_key_event( session_id: SessionID, name: String, platform_code: i32, @@ -490,7 +506,7 @@ pub fn session_handle_flutter_key_event( ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { let keyboard_mode = session.get_keyboard_mode(); - session.handle_flutter_key_event( + session.handle_flutter_raw_key_event( &keyboard_mode, &name, platform_code, @@ -585,6 +601,7 @@ pub fn session_send_files( file_num: i32, include_hidden: bool, is_remote: bool, + _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); @@ -616,7 +633,7 @@ pub fn session_remove_file( } } -pub fn session_read_dir_recursive( +pub fn session_read_dir_to_remove_recursive( session_id: SessionID, act_id: i32, path: String, @@ -662,6 +679,27 @@ pub fn session_read_local_dir_sync( "".to_string() } +pub fn session_read_local_empty_dirs_recursive_sync( + _session_id: SessionID, + path: String, + include_hidden: bool, +) -> String { + if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) { + return make_vec_fd_to_json(&fds); + } + "".to_string() +} + +pub fn session_read_remote_empty_dirs_recursive_sync( + session_id: SessionID, + path: String, + include_hidden: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.read_empty_dirs(path, include_hidden); + } +} + pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.get_platform(is_remote); @@ -701,6 +739,18 @@ pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { } } +pub fn session_rename_file( + session_id: SessionID, + act_id: i32, + path: String, + new_name: String, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.rename_file(act_id, path, new_name, is_remote); + } +} + pub fn session_elevate_direct(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.elevate_direct(); @@ -742,13 +792,6 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } -pub fn main_get_default_sound_input() -> Option { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return get_default_sound_input(); - #[cfg(any(target_os = "android", target_os = "ios"))] - None -} - pub fn main_get_login_device_info() -> SyncReturn { SyncReturn(get_login_device_info_json()) } @@ -788,11 +831,19 @@ pub fn main_show_option(_key: String) -> SyncReturn { pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { - crate::ui_cm_interface::notify_input_control(config::option2bool( - config::keys::OPTION_ENABLE_KEYBOARD, - &value, - )); + crate::ui_cm_interface::switch_permission_all( + "keyboard".to_owned(), + config::option2bool(&key, &value), + ); + } + #[cfg(target_os = "android")] + if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + crate::ui_cm_interface::switch_permission_all( + "clipboard".to_owned(), + config::option2bool(&key, &value), + ); } + if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); #[cfg(target_os = "android")] @@ -1311,6 +1362,14 @@ pub fn session_close_voice_call(session_id: SessionID) { } } +pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.get_conn_token()) + } else { + SyncReturn(None) + } +} + pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { crate::ui_cm_interface::handle_incoming_voice_call(id, accept); } @@ -1346,11 +1405,11 @@ pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } -pub fn main_get_software_update_url() -> String { - if get_local_option("enable-check-update".to_string()) != "N" { +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::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } pub fn main_get_home_dir() -> String { @@ -1568,6 +1627,7 @@ pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.toggle_virtual_display(index, on); + flutter::session_update_virtual_display(&session, index, on); } } @@ -1617,6 +1677,14 @@ pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } +pub fn main_get_unlock_pin() -> SyncReturn { + SyncReturn(get_unlock_pin()) +} + +pub fn main_set_unlock_pin(pin: String) -> SyncReturn { + SyncReturn(set_unlock_pin(pin)) +} + pub fn main_check_mouse_time() { check_mouse_time(); } @@ -1841,6 +1909,10 @@ pub fn install_install_path() -> SyncReturn { SyncReturn(install_path()) } +pub fn install_install_options() -> SyncReturn { + SyncReturn(install_options()) +} + pub fn main_account_auth(op: String, remember_me: bool) { let id = get_id(); let uuid = get_uuid(); @@ -1869,7 +1941,7 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } -pub fn main_hide_docker() -> SyncReturn { +pub fn main_hide_dock() -> SyncReturn { #[cfg(target_os = "macos")] crate::platform::macos::hide_dock(); SyncReturn(true) @@ -2220,26 +2292,52 @@ pub fn main_get_hard_option(key: String) -> SyncReturn { } pub fn main_get_buildin_option(key: String) -> SyncReturn { - SyncReturn(get_buildin_option(&key)) + SyncReturn(get_builtin_option(&key)) } pub fn main_check_hwcodec() { check_hwcodec() } +pub fn main_get_trusted_devices() -> String { + get_trusted_devices() +} + +pub fn main_remove_trusted_devices(json: String) { + remove_trusted_devices(&json) +} + +pub fn main_clear_trusted_devices() { + clear_trusted_devices() +} + +pub fn main_max_encrypt_len() -> SyncReturn { + SyncReturn(max_encrypt_len()) +} + pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.request_init_msgs(display); } } +pub fn main_audio_support_loopback() -> SyncReturn { + #[cfg(target_os = "windows")] + let is_surpport = true; + #[cfg(feature = "screencapturekit")] + let is_surpport = crate::audio_service::is_screen_capture_kit_available(); + #[cfg(not(any(target_os = "windows", feature = "screencapturekit")))] + let is_surpport = false; + SyncReturn(is_surpport) +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; use jni::{ errors::{Error as JniError, Result as JniResult}, objects::{JClass, JObject, JString}, - sys::jstring, + sys::{jboolean, jstring}, JNIEnv, }; @@ -2312,4 +2410,12 @@ pub mod server_side { }; return env.new_string(res).unwrap_or_default().into_raw(); } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( + env: JNIEnv, + _class: JClass, + ) -> jboolean { + jboolean::from(crate::server::is_clipboard_service_ok()) + } } diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index f2717a42ca19..91c18c4b206b 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -5,7 +5,7 @@ use std::{ }; #[cfg(not(any(target_os = "ios")))] -use crate::{ui_interface::get_buildin_option, Connection}; +use crate::{ui_interface::get_builtin_option, Connection}; use hbb_common::{ config::{keys, Config, LocalConfig}, tokio::{self, sync::broadcast, time::Instant}, @@ -91,11 +91,11 @@ async fn start_hbbs_sync_async() { if !ab_tag.is_empty() { v[keys::OPTION_PRESET_ADDRESS_BOOK_TAG] = json!(ab_tag); } - let username = get_buildin_option(keys::OPTION_PRESET_USERNAME); + let username = get_builtin_option(keys::OPTION_PRESET_USERNAME); if !username.is_empty() { v[keys::OPTION_PRESET_USERNAME] = json!(username); } - let strategy_name = get_buildin_option(keys::OPTION_PRESET_STRATEGY_NAME); + 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); } diff --git a/src/ipc.rs b/src/ipc.rs index 489db38e7098..f1deb5ba8e5c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, -}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; - use crate::{ privacy_mode::PrivacyModeState, ui_interface::{get_local_option, set_local_option}, @@ -14,6 +7,12 @@ use parity_tokio_ipc::{ Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, }; use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -26,8 +25,13 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, tokio, - tokio::io::{AsyncRead, AsyncWrite}, + log, password_security as password, + sodiumoxide::base64, + timeout, + tokio::{ + self, + io::{AsyncRead, AsyncWrite}, + }, tokio_util::codec::Framed, ResultType, }; @@ -41,6 +45,10 @@ pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { + ReadEmptyDirs { + dir: String, + include_hidden: bool, + }, ReadDir { dir: String, include_hidden: bool, @@ -98,6 +106,26 @@ pub enum FS { last_modified: u64, is_upload: bool, }, + Rename { + id: i32, + path: String, + new_name: String, + }, +} + +#[cfg(target_os = "windows")] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t")] +pub struct ClipboardNonFile { + pub compress: bool, + pub content: bytes::Bytes, + pub content_len: usize, + pub next_raw: bool, + pub width: i32, + pub height: i32, + // message.proto: ClipboardFormat + pub format: i32, + pub special_name: String, } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -189,8 +217,6 @@ pub enum Data { MouseMoveTime(i64), Authorize, Close, - #[cfg(target_os = "android")] - InputControl(bool), #[cfg(windows)] SAS, UserSid(Option), @@ -207,6 +233,8 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), + #[cfg(target_os = "windows")] + ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -238,10 +266,13 @@ pub enum Data { ControlledSessionCount(usize), CmErr(String), CheckHwcodec, + #[cfg(feature = "flutter")] VideoConnCount(Option), // Although the key is not neccessary, it is used to avoid hardcoding the key. WaylandScreencastRestoreToken((String, String)), HwCodecConfig(Option), + RemoveTrustedDevices(Vec), + ClearTrustedDevices, } #[tokio::main(flavor = "current_thread")] @@ -376,7 +407,8 @@ async fn handle(data: Data, stream: &mut Connection) { { hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) .await; - crate::run_me::<&str>(vec![]).ok(); + // https://github.com/rustdesk/rustdesk/discussions/9254 + crate::run_me::<&str>(vec!["--no-server"]).ok(); } #[cfg(target_os = "macos")] { @@ -424,6 +456,7 @@ async fn handle(data: Data, stream: &mut Connection) { log::info!("socks updated"); } }, + #[cfg(feature = "flutter")] Data::VideoConnCount(None) => { let n = crate::server::AUTHED_CONNS .lock() @@ -466,6 +499,10 @@ async fn handle(data: Data, stream: &mut Connection) { }; } else if name == "voice-call-input" { value = crate::audio_service::get_voice_call_input_device(); + } else if name == "unlock-pin" { + value = Some(Config::get_unlock_pin()); + } else if name == "trusted-devices" { + value = Some(Config::get_trusted_devices_json()); } else { value = None; } @@ -483,6 +520,8 @@ async fn handle(data: Data, stream: &mut Connection) { Config::set_salt(&value); } else if name == "voice-call-input" { crate::audio_service::set_voice_call_input_device(Some(value), true); + } else if name == "unlock-pin" { + Config::set_unlock_pin(&value); } else { return; } @@ -616,6 +655,12 @@ async fn handle(data: Data, stream: &mut Connection) { ); } } + Data::RemoveTrustedDevices(v) => { + Config::remove_trusted_devices(&v); + } + Data::ClearTrustedDevices => { + Config::clear_trusted_devices(); + } _ => {} } } @@ -844,6 +889,17 @@ pub async fn set_config_async(name: &str, value: String) -> ResultType<()> { Ok(()) } +#[tokio::main(flavor = "current_thread")] +pub async fn set_data(data: &Data) -> ResultType<()> { + set_data_async(data).await +} + +async fn set_data_async(data: &Data) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(data).await?; + Ok(()) +} + #[tokio::main(flavor = "current_thread")] pub async fn set_config(name: &str, value: String) -> ResultType<()> { set_config_async(name, value).await @@ -873,6 +929,68 @@ pub fn set_permanent_password(v: String) -> ResultType<()> { set_config("permanent-password", v) } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { + let v = v.trim().to_owned(); + let min_len = 4; + let max_len = crate::ui_interface::max_encrypt_len(); + let len = v.chars().count(); + if !v.is_empty() { + if len < min_len { + let err = if translate { + crate::lang::translate( + "Requires at least {".to_string() + &format!("{min_len}") + "} characters", + ) + } else { + // Sometimes, translated can't show normally in command line + format!("Requires at least {} characters", min_len) + }; + bail!(err); + } + if len > max_len { + bail!("No more than {max_len} characters"); + } + } + Config::set_unlock_pin(&v); + set_config("unlock-pin", v) +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_unlock_pin() -> String { + if let Ok(Some(v)) = get_config("unlock-pin") { + Config::set_unlock_pin(&v); + v + } else { + Config::get_unlock_pin() + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_trusted_devices() -> String { + if let Ok(Some(v)) = get_config("trusted-devices") { + v + } else { + Config::get_trusted_devices_json() + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn remove_trusted_devices(hwids: Vec) { + Config::remove_trusted_devices(&hwids); + allow_err!(set_data(&Data::RemoveTrustedDevices(hwids))); +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn clear_trusted_devices() { + Config::clear_trusted_devices(); + allow_err!(set_data(&Data::ClearTrustedDevices)); +} + pub fn get_id() -> String { if let Ok(Some(v)) = get_config("id") { // update salt also, so that next time reinstallation not causing first-time auto-login failure diff --git a/src/keyboard.rs b/src/keyboard.rs index 6b4b0988fb23..bc9ec8a771bc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -4,6 +4,7 @@ use crate::flutter; use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; +use crate::ui_session_interface::{InvokeUiSession, Session}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::{client::get_key_state, common::GrabState}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -34,6 +35,7 @@ const OS_LOWER_ANDROID: &str = "android"; #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); @@ -71,6 +73,7 @@ pub mod client { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { + #[cfg(feature = "flutter")] if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { return; } @@ -103,16 +106,31 @@ pub mod client { pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option) { let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); - if is_long_press(&event) { return; } - - for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { + let peer = get_peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { send_key_event(&key_event); } } + pub fn process_event_with_session( + keyboard_mode: &str, + event: &Event, + lock_modes: Option, + session: &Session, + ) { + let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); + if is_long_press(&event) { + return; + } + let peer = session.peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { + session.send_key_event(&key_event); + } + } + pub fn get_modifiers_state( alt: bool, ctrl: bool, @@ -169,6 +187,7 @@ pub mod client { } } + #[cfg(target_os = "android")] pub fn map_key_to_control_key(key: &rdev::Key) -> Option { match key { Key::Alt => Some(ControlKey::Alt), @@ -356,7 +375,6 @@ pub fn is_long_press(event: &Event) -> bool { return false; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn release_remote_keys(keyboard_mode: &str) { // todo!: client quit suddenly, how to release keys? let to_release = TO_RELEASE.lock().unwrap().clone(); @@ -385,7 +403,6 @@ pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode { } #[inline] -#[cfg(not(any(target_os = "ios")))] pub fn is_modifier(key: &rdev::Key) -> bool { matches!( key, @@ -401,7 +418,17 @@ pub fn is_modifier(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_modifier_code(evt: &KeyEvent) -> bool { + match evt.union { + Some(key_event::Union::Chr(code)) => { + let key = rdev::linux_key_from_code(code); + is_modifier(&key) + } + _ => false, + } +} + +#[inline] pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -424,7 +451,6 @@ pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -460,7 +486,6 @@ pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { // https://github.com/rustdesk/rustdesk/issues/8599 // We just add these keys as letter keys. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { matches!( key, @@ -469,7 +494,6 @@ pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_numpad_key(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key)) } @@ -477,12 +501,10 @@ fn is_numpad_key(event: &Event) -> bool { // Check is letter key for lock modes. // Only letter keys need to check and send Lock key state. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_letter_key_4_lock_modes(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if (is_letter_rdev_key(&key) || is_letter_rdev_key_ex(&key))) } -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn parse_add_lock_modes_modifiers( key_event: &mut KeyEvent, lock_modes: i32, @@ -553,10 +575,13 @@ fn update_modifiers_state(event: &Event) { } pub fn event_to_key_events( + mut peer: String, event: &Event, keyboard_mode: KeyboardMode, _lock_modes: Option, ) -> Vec { + peer.retain(|c| !c.is_whitespace()); + let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -570,16 +595,9 @@ pub fn event_to_key_events( _ => {} } - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - key_event.mode = keyboard_mode.into(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let mut key_events; - #[cfg(any(target_os = "android", target_os = "ios"))] - let key_events; - key_events = match keyboard_mode { + let mut key_events = match keyboard_mode { KeyboardMode::Map => map_keyboard_mode(peer.as_str(), event, key_event), KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { @@ -594,15 +612,14 @@ pub fn event_to_key_events( } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] let is_numpad_key = is_numpad_key(&event); - #[cfg(not(any(target_os = "android", target_os = "ios")))] if keyboard_mode != KeyboardMode::Translate || is_numpad_key { let is_letter_key = is_letter_key_4_lock_modes(&event); for key_event in &mut key_events { if let Some(lock_modes) = _lock_modes { parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key); } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key); } } @@ -615,6 +632,7 @@ pub fn send_key_event(key_event: &KeyEvent) { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { session.send_key_event(key_event); } + #[cfg(feature = "flutter")] if let Some(session) = flutter::get_cur_session() { session.send_key_event(key_event); @@ -862,24 +880,11 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Vec { - match _map_keyboard_mode(_peer, event, key_event) { - Some(key_event) => { - if _peer == OS_LOWER_LINUX { - if let EventType::KeyPress(k) = &event.event_type { - #[cfg(target_os = "ios")] - let try_workaround = true; - #[cfg(not(target_os = "ios"))] - let try_workaround = !is_modifier(k); - if try_workaround { - return try_workaround_linux_long_press(key_event); - } - } - } - vec![key_event] - } - None => Vec::new(), - } + _map_keyboard_mode(_peer, event, key_event) + .map(|e| vec![e]) + .unwrap_or_default() } fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { @@ -934,20 +939,23 @@ fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Op _ => event.position_code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] - let keycode = 0; - + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::usb_hid_code_to_win_scancode(event.usb_hid as _)?, + OS_LOWER_LINUX => rdev::usb_hid_code_to_linux_code(event.usb_hid as _)?, + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::usb_hid_code_to_macos_iso_code(event.usb_hid as _)? + } else { + rdev::usb_hid_code_to_macos_code(event.usb_hid as _)? + } + } + OS_LOWER_ANDROID => rdev::usb_hid_code_to_android_key_code(event.usb_hid as _)?, + _ => event.usb_hid as _, + }; key_event.set_chr(keycode as _); Some(key_event) } -// https://github.com/rustdesk/rustdesk/issues/6793 -#[inline] -fn try_workaround_linux_long_press(key_event: KeyEvent) -> Vec { - let mut key_event_up = key_event.clone(); - key_event_up.down = false; - vec![key_event, key_event_up] -} - #[cfg(not(any(target_os = "ios")))] fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { diff --git a/src/lan.rs b/src/lan.rs index 666e73c7c3dc..7d3f4f05fedd 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,4 +1,3 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::config::Config; use hbb_common::{ allow_err, @@ -22,7 +21,7 @@ use std::{ type Message = RendezvousMessage; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub(super) fn start_listening() -> ResultType<()> { let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let socket = std::net::UdpSocket::bind(addr)?; @@ -40,13 +39,22 @@ pub(super) fn start_listening() -> ResultType<()> { &Config::get_option("enable-lan-discovery"), ) { + let id = Config::get_id(); + if p.id == id { + continue; + } if let Some(self_addr) = get_ipaddr_by_peer(&addr) { let mut msg_out = Message::new(); + let mut hostname = whoami::hostname(); + // The default hostname is "localhost" which is a bit confusing + if hostname == "localhost" { + hostname = "unknown".to_owned(); + } let peer = PeerDiscovery { cmd: "pong".to_owned(), mac: get_mac(&self_addr), - id: Config::get_id(), - hostname: whoami::hostname(), + id, + hostname, username: crate::platform::get_active_username(), platform: whoami::platform().to_string(), ..Default::default() @@ -100,17 +108,17 @@ fn get_broadcast_port() -> u16 { } fn get_mac(_ip: &IpAddr) -> String { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Ok(mac) = get_mac_by_ip(_ip) { mac.to_string() } else { "".to_owned() } - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] "".to_owned() } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] fn get_mac_by_ip(ip: &IpAddr) -> ResultType { for interface in default_net::get_interfaces() { match ip { @@ -153,6 +161,10 @@ fn get_ipaddr_by_peer(peer: A) -> Option { fn create_broadcast_sockets() -> Vec { let mut ipv4s = Vec::new(); + // TODO: maybe we should use a better way to get ipv4 addresses. + // But currently, it's ok to use `[Ipv4Addr::UNSPECIFIED]` for discovery. + // `default_net::get_interfaces()` causes undefined symbols error when `flutter build` on iOS simulator x86_64 + #[cfg(not(any(target_os = "ios")))] for interface in default_net::get_interfaces() { for ipv4 in &interface.ipv4 { ipv4s.push(ipv4.addr.clone()); @@ -178,8 +190,20 @@ fn send_query() -> ResultType> { } let mut msg_out = Message::new(); + // We may not be able to get the mac address on mobile platforms. + // So we need to use the id to avoid discovering ourselves. + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = crate::ui_interface::get_id(); + // `crate::ui_interface::get_id()` will cause error: + // `get_id()` uses async code with `current_thread`, which is not allowed in this context. + // + // No need to get id for desktop platforms. + // We can use the mac address to identify the device. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let id = "".to_owned(); let peer = PeerDiscovery { cmd: "ping".to_owned(), + id, ..Default::default() }; msg_out.set_peer_discovery(peer); diff --git a/src/lang.rs b/src/lang.rs index b269d8631fc0..706822678037 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -41,7 +41,7 @@ mod sv; mod th; mod tr; mod tw; -mod ua; +mod uk; mod vn; pub const LANGS: &[(&str, &str)] = &[ @@ -72,7 +72,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ja", "日本語"), ("ko", "한국어"), ("kz", "Қазақ"), - ("ua", "Українська"), + ("uk", "Українська"), ("fa", "فارسی"), ("ca", "Català"), ("el", "Ελληνικά"), @@ -144,7 +144,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ja" => ja::T.deref(), "ko" => ko::T.deref(), "kz" => kz::T.deref(), - "ua" => ua::T.deref(), + "uk" => uk::T.deref(), "fa" => fa::T.deref(), "ca" => ca::T.deref(), "el" => el::T.deref(), diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 7fd258b81a1b..08eae5dbd5b0 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "عرض مراقب الجودة"), ("Disable clipboard", "تعطيل الحافظة"), ("Lock after session end", "القفل بعد نهاية هذه الجلسة"), - ("Insert", "ادخال"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del دخال"), ("Insert Lock", "قفل الادخال"), ("Refresh", "تحديث"), ("ID does not exist", "المعرف غير موجود"), @@ -364,6 +364,7 @@ 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", "ايقاف تسجيل الجلسة"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 78ed849ff9ac..c23143776090 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Паказваць манітор якасці"), ("Disable clipboard", "Адключыць буфер абмену"), ("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"), - ("Insert", "Уставіць"), + ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), ("Insert Lock", "Заблакаваць уліковы запіс"), ("Refresh", "Абнавіць"), ("ID does not exist", "ID не існуе"), @@ -364,6 +364,7 @@ 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", "Спыніць запіс сесіі"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index cddbddac7e6c..469b1b4fb39e 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -1,19 +1,19 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Статус"), - ("Your Desktop", "Твоят Работен Плот"), - ("desk_tip", "Вашият работен плот може да бъде достъпен с този идентификационен код и парола."), + ("Status", "Положение"), + ("Your Desktop", "Вашата работна среда"), + ("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."), ("Password", "Парола"), ("Ready", "Готово"), ("Established", "Установен"), ("connecting_status", "Свързване с RustDesk мрежата..."), - ("Enable service", "Пусни услуга"), + ("Enable service", "Разреши услуга"), ("Start service", "Стартирай услуга"), ("Service is running", "Услугата работи"), ("Service is not running", "Услугата не работи"), ("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"), - ("Control Remote Desktop", "Контролирайте отдалечения работен плот"), + ("Control Remote Desktop", "Отдалечено управление на работна среда"), ("Transfer file", "Прехвърляне на файл"), ("Connect", "Свързване"), ("Recent sessions", "Последни сесии"), @@ -23,27 +23,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Невалидна конфигурация на сървъра"), + ("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"), + ("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", "Без звук"), @@ -53,23 +53,23 @@ 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", "Релейн сървър"), + ("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", "Опитайте отново"), + ("Close", "Затваряне"), + ("Retry", "Преповтори"), ("OK", "Добре"), ("Password Required", "Изисква се парола"), - ("Please enter your password", "Моля въведете паролата си"), + ("Please enter your password", "Моля въведете парола"), ("Remember password", "Запомни паролата"), ("Wrong Password", "Грешна парола"), ("Do you want to enter again?", "Искате ли да въведете отново?"), @@ -99,73 +99,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Properties", "Свойства"), ("Multi Select", "Множествен избор"), ("Select All", "Избери всички"), - ("Unselect All", "Деселектирай всички"), - ("Empty Directory", "Празна директория"), - ("Not an empty directory", "Не е празна директория"), + ("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!", ""), + ("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", "Регулирай прозореца"), + ("Block user input", "Забрана за потребителски вход"), + ("Unblock user input", "Разрешаване на потребителски въвеждане"), + ("Adjust Window", "Нагласи прозореца"), ("Original", "Оригинално"), ("Shrink", "Свиване"), ("Stretch", "Разтегнат"), ("Scrollbar", "Плъзгач"), - ("ScrollAuto", "Автоматичен плъзгач"), + ("ScrollAuto", "Автоматичено приплъзване"), ("Good image quality", "Добро качество на изображението"), - ("Balanced", "Балансиран"), - ("Optimize reaction time", "Оптимизирайте времето за реакция"), - ("Custom", "Персонализиран"), - ("Show remote cursor", "Показване на дистанционния курсор"), - ("Show quality monitor", "Показване на прозорец за качество"), - ("Disable clipboard", "Деактивиране на клипборда"), - ("Lock after session end", "Заключване след края на сесията"), - ("Insert", "Поставяне"), + ("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", "Неуспешно свързване със сървъра за рандеву"), + ("Refresh", "Обновяване"), + ("ID does not exist", "Несъществуващ определител (ID)"), + ("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"), ("Please try later", "Моля опитайте по-късно"), - ("Remote desktop is offline", "Отдалеченият работен плот е офлайн"), + ("Remote desktop is offline", "Отдалечената работна среда не е налична"), ("Key mismatch", "Ключово несъответствие"), - ("Timeout", ""), - ("Failed to connect to relay server", ""), - ("Failed to connect via rendezvous server", ""), - ("Failed to connect via relay server", ""), - ("Failed to make direct connection to remote desktop", ""), - ("Set Password", "Задайте парола"), + ("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", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), - ("Installing ...", "Инсталиране..."), - ("Install", "Инсталирай"), - ("Installation", "Инсталация"), - ("Installation Path", "Инсталационен път"), - ("Create start menu shortcuts", "Създайте преки пътища в менюто 'Старт'."), + ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."), + ("Click to upgrade", "Натиснете, за да надстроите"), + ("Click to download", "Натиснете, за да изтеглите"), + ("Click to update", "Натиснете, за да обновите"), + ("Configure", "Настройване"), + ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."), + ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."), + ("Installing ...", "Поставяне..."), + ("Install", "Постави"), + ("Installation", "Поставяне"), + ("Installation Path", "Път към място за поставяне"), + ("Create start menu shortcuts", "Бърз достъп от меню 'Старт'."), ("Create desktop icon", "Създайте икона на работния плот"), - ("agreement_tip", "Стартирайки инсталацията, вие приемате лицензионното споразумение."), - ("Accept and Install", "Приемете и инсталирайте"), - ("End-user license agreement", ""), - ("Generating ...", "Генериране..."), + ("agreement_tip", "Започвайки поставянето, вие приемате лицензионното споразумение."), + ("Accept and Install", "Приемете и поставяте"), + ("End-user license agreement", "Споразумение с потребителя"), + ("Generating ...", "Пораждане..."), ("Your installation is lower version.", "Вашата инсталация е по-ниска версия."), ("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"), ("Listening ...", "Слушане..."), - ("Remote Host", "Отдалечен хост"), + ("Remote Host", "Отдалечен сървър"), ("Remote Port", "Отдалечен порт"), ("Action", "Действие"), ("Add", "Добави"), @@ -173,154 +173,154 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "Локален адрес"), ("Change Local Port", "Промяна на локалният порт"), ("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), + ("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", "Въведете паролата си"), - ("Logging in...", ""), - ("Enable RDP session sharing", "Активирайте споделянето на RDP сесия"), + ("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 достъп"), + ("Enable direct IP access", "Разрешаване пряк IP достъп"), ("Rename", "Преименуване"), ("Space", "Пространство"), ("Create desktop shortcut", "Създайте пряк път на работния плот"), ("Change Path", "Промяна на пътя"), ("Create Folder", "Създай папка"), - ("Please enter the folder name", "Моля, въведете името на папката"), + ("Please enter the folder name", "Моля, въведете име на папката"), ("Fix it", "Оправи го"), ("Warning", "Внимание"), - ("Login screen using Wayland is not supported", "Екранът за влизане с помощта на Wayland не се поддържа"), - ("Reboot required", "Изисква се рестартиране"), - ("Unsupported display server", "Неподдържан сървър за дисплея"), - ("x11 expected", ""), + ("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", "Затворено ръчно от партньора"), + ("Invalid port", "Недопустим порт"), + ("Closed manually by the peer", "Затворено ръчно от другата страна"), ("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"), ("Run without install", "Стартирайте без инсталиране"), - ("Connect via relay", "Свържете чрез реле"), - ("Always connect via relay", "Винаги свързвайте чрез реле"), + ("Connect via relay", "Свързване чрез препращане"), + ("Always connect via relay", "Винаги чрез препращане"), ("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"), ("Login", "Влизане"), ("Verify", "Потвърди"), ("Remember me", "Запомни ме"), - ("Trust this device", "Доверете се на това устройство"), + ("Trust this device", "Доверяване на това устройство"), ("Verification code", "Код за потвърждение"), - ("verification_tip", "На регистрирания имейл адрес е изпратен код за потвърждение, въведете кода за потвърждение, за да продължите да влизате."), - ("Logout", "Излез от профила си"), - ("Tags", "Етикети"), - ("Search ID", "Търсене на ID"), - ("whitelist_sep", "Разделени със запетая, точка и запетая, интервали или нов ред"), + ("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с влизането."), + ("Logout", "Отписване (Изход)"), + ("Tags", "Белези"), + ("Search ID", "Търси ID"), + ("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"), ("Add ID", "Добави ID"), ("Add Tag", "Добави етикет"), - ("Unselect all tags", "Премахнете избора на всички етикети"), + ("Unselect all tags", "Премахнете избора на всички белези (tags)"), ("Network error", "Мрежова грешка"), - ("Username missed", "Пропуснато потребителско име"), - ("Password missed", "Пропусната парола"), - ("Wrong credentials", "Wrong username or password"), - ("The verification code is incorrect or has expired", ""), - ("Edit Tag", "Edit tag"), + ("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", ""), - ("Socks5 Proxy", "Socks5 прокси"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), - ("Discovered", ""), - ("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), - ("Remote ID", "Дистанционно ID"), + ("Invalid folder name", "Непозволено име на папка"), + ("Socks5 Proxy", "Socks5 посредник"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) посредник"), + ("Discovered", "Открит"), + ("install_daemon_tip", "За зареждане при стартиране на ОС следва да поставите RustDesk като системна услуга."), + ("Remote ID", "Отдалечено ID"), ("Paste", "Постави"), ("Paste here?", "Постави тук?"), ("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"), - ("Download new version", ""), - ("Touch mode", "Режим тъч (сензорен)"), + ("Download new version", "Изтегляне на нова версия"), + ("Touch mode", "Режим сензорен (touch)"), ("Mouse mode", "Режим мишка"), - ("One-Finger Tap", "Докосване с един пръст"), + ("One-Finger Tap", "Допир с един пръст"), ("Left Mouse", "Ляв бутон на мишката"), - ("One-Long Tap", "Едно дълго докосване"), - ("Two-Finger Tap", "Докосване с два пръста"), + ("One-Long Tap", "Дълъг допир"), + ("Two-Finger Tap", "Допир с два пръста"), ("Right Mouse", "Десен бутон на мишката"), ("One-Finger Move", "Преместване с един пръст"), - ("Double Tap & Move", "Докоснете два пъти и преместете"), - ("Mouse Drag", "Плъзгане с мишката"), + ("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", ""), + ("Reset canvas", "Нулиране на платното"), + ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"), + ("Note", "Бележка"), + ("Connection", "Връзка"), ("Share Screen", "Сподели екран"), - ("Chat", "Чат"), - ("Total", "Обшо"), - ("items", "елементи"), + ("Chat", "Говор"), + ("Total", "Общо"), + ("items", "неща"), ("Selected", "Избрано"), - ("Screen Capture", "Заснемане на екрана"), - ("Input Control", "Контрол на въвеждане"), - ("Audio Capture", "Аудио записване"), + ("Screen Capture", "Снемане на екрана"), + ("Input Control", "Управление на вход"), + ("Audio Capture", "Аудиозапис"), ("File Connection", "Файлова връзка"), - ("Screen Connection", "Свързване на екрана"), + ("Screen Connection", "Екранна връзка"), ("Do you accept?", "Приемате ли?"), - ("Open System Setting", "Отворете системната настройка"), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или докосване, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), + ("Open System Setting", "Отворете системните настройки"), + ("How to get Android input permission?", "Как да получим право за въвеждане под Андрид?"), + ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), ("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."), - ("android_new_connection_tip", "Получена е нова заявка за контрол, която иска да контролира вашето текущо устройство."), - ("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), + ("android_new_connection_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", "Акаунт"), + ("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."), + ("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."), + ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."), + ("Account", "Сметка"), ("Overwrite", "Презаписване"), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", "Излез"), + ("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", ""), + ("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", ""), + ("Keep RustDesk background service", "Запази работеща фонова услуга с RustDesk"), ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"), ("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), ("Start on boot", "Стартирайте при зареждане"), ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", "Режим на превод"), - ("Use permanent password", "Използвайте постоянна парола"), - ("Use both passwords", "Използвайте и двете пароли"), - ("Set permanent password", "Задайте постоянна парола"), - ("Enable remote restart", "Разрешете отдалечено рестартиране"), - ("Restart remote device", "Рестартирайте отдалеченото устройство"), + ("Connection not allowed", "Връзката непозволена"), + ("Legacy mode", "По остарял начин"), + ("Map mode", "По начин със съответствие (map)"), + ("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", "Рестартира се отдалечено устройство"), + ("Restarting remote device", "Рестартиране на отдалечено устройство"), ("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"), - ("Copied", "Копирано"), + ("Copied", "Преписано"), ("Exit Fullscreen", "Изход от цял екран"), ("Fullscreen", "Цял екран"), ("Mobile Actions", "Мобилни действия"), @@ -334,10 +334,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide Toolbar", "Скриване на лентата с инструменти"), ("Direct Connection", "Директна връзка"), ("Relay Connection", "Релейна връзка"), - ("Secure Connection", "Защитена връзка"), - ("Insecure Connection", "Незащитена връзка"), + ("Secure Connection", "Сигурна връзка"), + ("Insecure Connection", "Несигурна връзка"), ("Scale original", "Оригинален мащаб"), - ("Scale adaptive", "Адаптивно мащабиране"), + ("Scale adaptive", "Приспособимо мащабиране"), ("General", "Основен"), ("Security", "Сигурност"), ("Theme", "Тема"), @@ -345,128 +345,129 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Светла тема"), ("Dark", "Тъмна"), ("Light", "Светла"), - ("Follow System", "Следвай системата"), - ("Enable hardware codec", "Активиране на хардуерен кодек"), + ("Follow System", "Следвай система"), + ("Enable hardware codec", "Позволяване хардуерен кодек"), ("Unlock Security Settings", "Отключи настройките за сигурност"), ("Enable audio", "Разрешете аудиото"), ("Unlock Network Settings", "Отключи мрежовите настройки"), ("Server", "Сървър"), - ("Direct IP Access", "Директен IP достъп"), - ("Proxy", "Прокси"), - ("Apply", "Приложи"), - ("Disconnect all devices?", ""), - ("Clear", "Изчисти"), + ("Direct IP Access", "Пряк IP достъп"), + ("Proxy", "Посредник (Proxy)"), + ("Apply", "Прилагане"), + ("Disconnect all devices?", "Разкачване на всички устройства"), + ("Clear", "Изчистване"), ("Audio Input Device", "Аудио входно устройство"), - ("Use IP Whitelisting", "Използвайте бял списък с IP адреси"), + ("Use IP Whitelisting", "Използване бял списък с IP адреси"), ("Network", "Мрежа"), - ("Pin Toolbar", "Фиксиране на лентата с инструменти"), - ("Unpin Toolbar", "Откачване на лентата с инструменти"), + ("Pin Toolbar", "Закачане лента с инструменти"), + ("Unpin Toolbar", "Откачюане лента с инструменти"), ("Recording", "Записване"), ("Directory", "Директория"), - ("Automatically record incoming sessions", ""), - ("Change", "Промени"), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", "Активирайте откриване в LAN"), - ("Deny LAN discovery", "Забранете откриване в LAN"), + ("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...", ""), + ("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."), ("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."), ("Disconnected", "Прекъсната връзка"), ("Other", "Други"), - ("Confirm before closing multiple tabs", ""), + ("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", "Преглед"), - ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (Работете от страна на партньора)."), + ("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...", ""), + ("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 support is in experimental stage, please use X11 if you require unattended access."), - ("Right click to select tabs", ""), + ("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", ""), + ("Add to address book", "Добавяне към познати адреси"), ("Group", "Група"), ("Search", "Търсене"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("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 разрешения \"Input Monitoring\"."), - ("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", "Поискайте повишаване на привилегии"), + ("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"), + ("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."), + ("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", ""), - ("Switch Sides", "Сменете страните"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Elevate successfully", "Успешно получаване на разширени права"), + ("uppercase", "големи букви"), + ("lowercase", "малки букви"), + ("digit", "цифра"), + ("special character", "специален знак"), + ("length>=8", "дължина>=8"), + ("Weak", "Слаба"), + ("Medium", "Средна"), + ("Strong", "Силна"), + ("Switch Sides", "Размяна на страните"), + ("Please confirm if you want to share your desktop?", "Моля, потвърдете дали искате да споделите работното си пространство"), + ("Display", "Екран"), ("Default View Style", "Стил на изглед по подразбиране"), ("Default Scroll Style", "Стил на превъртане по подразбиране"), ("Default Image Quality", "Качество на изображението по подразбиране"), ("Default Codec", "Кодек по подразбиране"), - ("Bitrate", "Битрейт"), + ("Bitrate", "Скорост на предаване на данни (bitrate)"), ("FPS", "Кадри в секунда"), ("Auto", "Автоматично"), ("Other Default Options", "Други опции по подразбиране"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Гласови обаждания"), + ("Text chat", "Текстов разговор"), + ("Stop voice call", "Прекратяване гласово обаждане"), ("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."), - ("Reconnect", "Свържете се отново"), + ("Reconnect", "Повторно свързане"), ("Codec", "Кодек"), - ("Resolution", "Резолюция"), - ("No transfers in progress", "Не се извършват трансфери"), - ("Set one-time password length", ""), + ("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_lan_tip", "О, не, изглежда, че все още не сме открили връстници."), ("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."), - ("eg: admin", ""), + ("eg: admin", "напр. admin"), ("Empty Username", "Празно потребителско име"), ("Empty Password", "Празна парола"), - ("Me", "Аз"), - ("identical_file_tip", "Този файл е идентичен с този на партньора."), + ("Me", "Мен"), + ("identical_file_tip", "Файлът съвпада с този от другата страна."), ("show_monitors_tip", "Показване на мониторите в лентата с инструменти"), ("View Mode", "Режим на преглед"), ("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"), @@ -482,47 +483,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"), ("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", ""), + ("Select a peer", "Избери отдалечена страна"), + ("Select peers", "Избери отдалечени страни"), + ("Plugins", "Приставки"), + ("Uninstall", "Премахни"), + ("Update", "Обновяване"), + ("Enable", "Позволяване"), + ("Disable", "Забрана"), ("Options", "Настроики"), - ("resolution_original_tip", "Оригинална резолюция"), - ("resolution_fit_local_tip", "Напасване към локална разделителна способност"), - ("resolution_custom_tip", "Персонализирана разделителна способност"), + ("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", "Отвори"), + ("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", ""), + ("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", "Неуспешно синхронизиране на адресната книга със сървъра"), @@ -530,106 +531,130 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change Color", "Промяна на цвета"), ("Primary Color", "Основен цвят"), ("HSV Color", "HSV цвят"), - ("Installation Successful!", "Успешна инсталация!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), + ("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", ""), + ("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", ""), + ("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", ""), + ("Change view", "Промяна изглед"), + ("Big tiles", "Големи заглавия"), + ("Small tiles", "Малки заглавия"), + ("List", "Списък"), + ("Virtual display", "Виртуален екран"), + ("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\" , ключът не е необходим за публичен сървър"), ("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"), ("Swap control-command key", ""), - ("swap-left-right-mouse", "Разменете левия и десния бутон на мишката"), - ("2FA code", "Код за Двуфакторна удостоверяване"), + ("swap-left-right-mouse", "Размяна на копчетата на мишката"), + ("2FA code", "Код за Двуфакторно удостоверяване"), ("More", "Повече"), - ("enable-2fa-title", "Активиране на двуфакторно удостоверяване"), + ("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.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), + ("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."), + ("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"), + ("Multiple Windows sessions found", "Установени са няколко Windwos сесии"), + ("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", ""), + ("Security Alert", "Предупреждение за сигурност"), + ("My address book", "Моята адресна книга"), + ("Personal", "Личен"), + ("Owner", "Собственик"), + ("Set shared password", "Определяне споделена парола"), + ("Exist in", "Съществува в"), + ("Read-only", "Само четене"), + ("Read/Write", "Писане/четене"), + ("Full Control", "Пълен контрол"), ("share_warning_tip", ""), - ("Everyone", ""), + ("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", ""), + ("Follow remote cursor", "Следвай отдалечения курсор"), + ("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"), ("default_proxy_tip", ""), ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), + ("Incoming", "Входящ"), + ("Outgoing", "Изходящ"), + ("Clear Wayland screen selection", "Изчистване избор на Wayland екран"), ("clear_Wayland_screen_selection_tip", ""), ("confirm_clear_Wayland_screen_selection_tip", ""), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), + ("Use texture rendering", "Използвай текстово изграждане"), + ("Floating window", "Плаващ прозорец"), ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), + ("Keep screen on", "Запази екранът включен"), + ("Never", "Никога"), + ("During controlled", "Докато е обект на управление"), + ("During service is on", "Докато услугата е включена"), + ("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"), ("Back", "Назад"), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), + ("Apps", "Приложения"), + ("Volume up", "Усилване звук"), + ("Volume down", "Намаляне звук"), + ("Power", "Мощност"), + ("Telegram bot", "Телеграм бот"), ("enable-bot-tip", ""), ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f376d91df6e1..33992a3386bb 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -2,634 +2,659 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Estat"), - ("Your Desktop", "El teu escriptori"), - ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Your Desktop", "Aquest ordinador"), + ("desk_tip", "Es pot accedir a aquest equip mitjançant les credencials:"), ("Password", "Contrasenya"), - ("Ready", "Llest"), - ("Established", "Establert"), - ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable service", "Habilitar Servei"), - ("Start service", "Iniciar Servei"), - ("Service is running", "El servei s'està executant"), - ("Service is not running", "El servei no s'està executant"), - ("not_ready_status", "No està llest. Comprova la teva connexió"), - ("Control Remote Desktop", "Controlar escriptori remot"), - ("Transfer file", "Transferir arxiu"), - ("Connect", "Connectar"), + ("Ready", "Preparat."), + ("Established", "S'ha establert."), + ("connecting_status", "S'està connectant a la xarxa de RustDesk..."), + ("Enable service", "Habilita el servei."), + ("Start service", "Inicia el servei."), + ("Service is running", "El servei s'està executant."), + ("Service is not running", "El servei no s'està executant."), + ("not_ready_status", "No disponible. Verifiqueu la connexió"), + ("Control Remote Desktop", "Dispositiu remot"), + ("Transfer file", "Transfereix fitxers"), + ("Connect", "Connecta"), ("Recent sessions", "Sessions recents"), - ("Address book", "Directori"), + ("Address book", "Llibreta d'adreces"), ("Confirmation", "Confirmació"), ("TCP tunneling", "Túnel TCP"), - ("Remove", "Eliminar"), - ("Refresh random password", "Actualitzar contrasenya aleatòria"), - ("Set your own password", "Estableix la teva pròpia contrasenya"), - ("Enable keyboard/mouse", "Habilitar teclat/ratolí"), - ("Enable clipboard", "Habilitar portapapers"), - ("Enable file transfer", "Habilitar transferència d'arxius"), - ("Enable TCP tunneling", "Habilitar túnel TCP"), + ("Remove", "Suprimeix"), + ("Refresh random password", "Actualitza la contrasenya aleatòria"), + ("Set your own password", "Establiu la vostra contrasenya"), + ("Enable keyboard/mouse", "Habilita el teclat/ratolí"), + ("Enable clipboard", "Habilita el porta-retalls"), + ("Enable file transfer", "Habilita la transferència de fitxers"), + ("Enable TCP tunneling", "Habilita el túnel TCP"), ("IP Whitelisting", "Adreces IP admeses"), - ("ID/Relay Server", "Servidor ID/Relay"), - ("Import server config", "Importar configuració de servidor"), - ("Export Server Config", "Exportar configuració del servidor"), - ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), - ("Invalid server configuration", "Configuració de servidor incorrecta"), - ("Clipboard is empty", "El portapapers està buit"), - ("Stop service", "Aturar servei"), - ("Change ID", "Canviar ID"), - ("Your new ID", "La teva nova ID"), - ("length %min% to %max%", "Ha de tenir entre %min% i %max% caràcters"), - ("starts with a letter", "començar amb una lletra"), - ("allowed characters", "caràcters permesos"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), + ("ID/Relay Server", "ID/Repetidor del Servidor"), + ("Import server config", "Importa la configuració del servidor"), + ("Export Server Config", "Exporta la configuració del servidor"), + ("Import server configuration successfully", "S'ha importat la configuració del servidor correctament"), + ("Export server configuration successfully", "S'ha exportat la configuració del servidor correctament"), + ("Invalid server configuration", "Configuració del servidor no vàlida"), + ("Clipboard is empty", "El porta-retalls és buit"), + ("Stop service", "Atura el servei"), + ("Change ID", "Canvia la ID"), + ("Your new ID", "Identificador nou"), + ("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."), ("Website", "Lloc web"), - ("About", "Sobre"), - ("Slogan_tip", ""), - ("Privacy Statement", "Declaració de privacitat"), - ("Mute", "Silenciar"), - ("Build Date", "Data de creació"), + ("About", "Quant al RustDesk"), + ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), + ("Privacy Statement", "Declaració de privadesa"), + ("Mute", "Silencia"), + ("Build Date", "Data de compilació"), ("Version", "Versió"), ("Home", "Inici"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), - ("Hardware Codec", "Còdec de hardware"), - ("Adaptive bitrate", "Tasa de bits adaptativa"), - ("ID Server", "Servidor de IDs"), - ("Relay Server", "Servidor Relay"), - ("API Server", "Servidor API"), + ("Hardware Codec", "Codificació per maquinari"), + ("Adaptive bitrate", "Taxa de bits adaptativa"), + ("ID Server", "ID del servidor"), + ("Relay Server", "Repetidor del servidor"), + ("API Server", "Clau API del servidor"), ("invalid_http", "ha de començar amb http:// o https://"), - ("Invalid IP", "IP incorrecta"), - ("Invalid format", "Format incorrecte"), - ("server_not_support", "Encara no és compatible amb el servidor"), + ("Invalid IP", "IP no vàlida"), + ("Invalid format", "Format no vàlid"), + ("server_not_support", "Encara no suportat pel servidor"), ("Not available", "No disponible"), - ("Too frequent", "Massa comú"), - ("Cancel", "Cancel·lar"), - ("Skip", "Saltar"), - ("Close", "Tancar"), - ("Retry", "Reintentar"), + ("Too frequent", "Massa freqüent"), + ("Cancel", "Cancel·la"), + ("Skip", "Omet"), + ("Close", "Surt"), + ("Retry", "Torna a provar"), ("OK", "D'acord"), - ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), - ("Remember password", "Recordar contrasenya"), - ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vol tornar a entrar?"), + ("Password Required", "Contrasenya requerida"), + ("Please enter your password", "Inseriu la contrasenya"), + ("Remember password", "Recorda la contrasenya"), + ("Wrong Password", "Contrasenya no vàlida"), + ("Do you want to enter again?", "Voleu tornar a provar?"), ("Connection Error", "Error de connexió"), ("Error", "Error"), - ("Reset by the peer", "Reestablert pel peer"), - ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), - ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inicio de sessió"), - ("Successful", "Exitós"), - ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Reset by the peer", "Restablert pel client"), + ("Connecting...", "S'està connectant..."), + ("Connection in progress. Please wait.", "S'està connectant. Espereu..."), + ("Please try 1 minute later", "Torneu a provar en 1 minut"), + ("Login Error", "Error d'accés"), + ("Successful", "Correcte"), + ("Connected, waiting for image...", "S'ha connectat; en espera de rebre la imatge..."), ("Name", "Nom"), ("Type", "Tipus"), ("Modified", "Modificat"), - ("Size", "Grandària"), - ("Show Hidden Files", "Mostrar arxius ocults"), - ("Receive", "Rebre"), - ("Send", "Enviar"), - ("Refresh File", "Actualitzar arxiu"), + ("Size", "Mida"), + ("Show Hidden Files", "Mostra els fitxers ocults"), + ("Receive", "Rep"), + ("Send", "Envia"), + ("Refresh File", "Actualitza"), ("Local", "Local"), ("Remote", "Remot"), - ("Remote Computer", "Ordinador remot"), - ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma eliminació"), - ("Delete", "Eliminar"), + ("Remote Computer", "Dispositiu remot"), + ("Local Computer", "Aquest ordinador"), + ("Confirm Delete", "Confirmació de supressió"), + ("Delete", "Suprimeix"), ("Properties", "Propietats"), ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho Tot"), - ("Unselect All", "Deselecciona-ho Tot"), - ("Empty Directory", "Directori buit"), - ("Not an empty directory", "No és un directori buit"), - ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), - ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), - ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), - ("Do this for all conflicts", "Fes això per a tots els conflictes"), - ("This is irreversible!", "Això és irreversible!"), - ("Deleting", "Eliminant"), - ("files", "arxius"), - ("Waiting", "Esperant"), - ("Finished", "Acabat"), + ("Select All", "Seleciona-ho tot"), + ("Unselect All", "Desselecciona-ho tot"), + ("Empty Directory", "Carpeta buida"), + ("Not an empty directory", "No és una carpeta buida"), + ("Are you sure you want to delete this file?", "Segur que voleu suprimir aquest fitxer?"), + ("Are you sure you want to delete this empty directory?", "Segur que voleu suprimir aquesta carpeta buida?"), + ("Are you sure you want to delete the file of this directory?", "Segur que voleu suprimir el fitxer d'aquesta carpeta?"), + ("Do this for all conflicts", "Aplica aquesta acció per a tots els conflictes"), + ("This is irreversible!", "Aquesta acció no es pot desfer!"), + ("Deleting", "S'està suprimint"), + ("files", "fitxers"), + ("Waiting", "En espera"), + ("Finished", "Ha finalitzat"), ("Speed", "Velocitat"), ("Custom Image Quality", "Qualitat d'imatge personalitzada"), ("Privacy mode", "Mode privat"), - ("Block user input", "Bloquejar entrada d'usuari"), - ("Unblock user input", "Desbloquejar entrada d'usuari"), - ("Adjust Window", "Ajustar finestra"), + ("Block user input", "Bloca el control a l'usuari"), + ("Unblock user input", "Desbloca el control a l'usuari"), + ("Adjust Window", "Ajusta la finestra"), ("Original", "Original"), - ("Shrink", "Reduir"), - ("Stretch", "Estirar"), + ("Shrink", "Encongida"), + ("Stretch", "Ampliada"), ("Scrollbar", "Barra de desplaçament"), - ("ScrollAuto", "Desplaçament automàtico"), + ("ScrollAuto", "Desplaçament automàtic"), ("Good image quality", "Bona qualitat d'imatge"), - ("Balanced", "Equilibrat"), - ("Optimize reaction time", "Optimitzar el temps de reacció"), - ("Custom", "Personalitzat"), - ("Show remote cursor", "Mostrar cursor remot"), - ("Show quality monitor", "Mostrar qualitat del monitor"), - ("Disable clipboard", "Deshabilitar portapapers"), - ("Lock after session end", "Bloquejar després del final de la sessió"), - ("Insert", "Inserir"), - ("Insert Lock", "Inserir bloqueig"), - ("Refresh", "Actualitzar"), - ("ID does not exist", "L'ID no existeix"), - ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Siusplau provi-ho més tard"), - ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("Balanced", "Equilibrada"), + ("Optimize reaction time", "Optimitza el temps de reacció"), + ("Custom", "Personalitzada"), + ("Show remote cursor", "Mostra el cursor remot"), + ("Show quality monitor", "Mostra la informació de flux"), + ("Disable clipboard", "Inhabilita el porta-retalls"), + ("Lock after session end", "Bloca en finalitzar la sessió"), + ("Insert Ctrl + Alt + Del", "Insereix Ctrl + Alt + Del"), + ("Insert Lock", "Bloca"), + ("Refresh", "Actualitza"), + ("ID does not exist", "Aquesta ID no existeix"), + ("Failed to connect to rendezvous server", "Ha fallat en connectar al servidor assignat"), + ("Please try later", "Proveu més tard"), + ("Remote desktop is offline", "El dispositiu remot està desconnectat"), ("Key mismatch", "La clau no coincideix"), - ("Timeout", "Temps esgotat"), - ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), - ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), - ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), - ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configurar la contrasenya"), - ("OS Password", "contrasenya del sistema operatiu"), - ("install_tip", ""), - ("Click to upgrade", "Clicar per actualitzar"), - ("Click to download", "Clicar per descarregar"), - ("Click to update", "Clicar per refrescar"), - ("Configure", "Configurar"), - ("config_acc", ""), - ("config_screen", "Configurar pantalla"), - ("Installing ...", "Instal·lant ..."), - ("Install", "Instal·lar"), + ("Timeout", "S'ha exhaurit el temps"), + ("Failed to connect to relay server", "Ha fallat en connectar amb el repetidor del servidor"), + ("Failed to connect via rendezvous server", "Ha fallat en connectar mitjançant el servidor assignat"), + ("Failed to connect via relay server", "Ha fallat en connectar mitjançant el repetidor del servidor"), + ("Failed to make direct connection to remote desktop", "Ha fallat la connexió directa amb el dispositiu remot"), + ("Set Password", "Establiu una contrasenya"), + ("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."), + ("Installing ...", "S'està instal·lant..."), + ("Install", "Instal·la"), ("Installation", "Instal·lació"), - ("Installation Path", "Ruta d'instal·lació"), - ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), - ("Create desktop icon", "Crear icona d'escriptori"), - ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·lar"), - ("End-user license agreement", "Acord de llicència d'usuario final"), - ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), - ("not_close_tcp_tip", ""), - ("Listening ...", "Escoltant..."), - ("Remote Host", "Hoste remot"), + ("Installation Path", "Ruta de la instal·lació"), + ("Create start menu shortcuts", "Crea una drecera al menú d'inici"), + ("Create desktop icon", "Crea una icona a l'escriptori"), + ("agreement_tip", "En iniciar la instal·lació, esteu acceptant l'acord de llicència d'usuari."), + ("Accept and Install", "Accepta i instal·la"), + ("End-user license agreement", "Acord de llicència d'usuari final"), + ("Generating ...", "S'està generant..."), + ("Your installation is lower version.", "La instal·lació actual és una versió inferior"), + ("not_close_tcp_tip", "No tanqueu aquesta finestra mentre utilitzeu el túnel"), + ("Listening ...", "S'està escoltant..."), + ("Remote Host", "Amfitrió remot"), ("Remote Port", "Port remot"), ("Action", "Acció"), - ("Add", "Afegirr"), + ("Add", "Afegeix"), ("Local Port", "Port local"), - ("Local Address", "Adreça Local"), - ("Change Local Port", "Canviar Port Local"), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmación no coincideix."), + ("Local Address", "Adreça local"), + ("Change Local Port", "Canvia el port local"), + ("setup_server_tip", "Per a connexions més ràpides o privades, configureu el vostre servidor"), + ("Too short, at least 6 characters.", "Massa curt. Són necessaris almenys 6 caràcters."), + ("The confirmation is not identical.", "Les contrasenyes no coincideixen."), ("Permissions", "Permisos"), - ("Accept", "Acceptar"), - ("Dismiss", "Cancel·lar"), - ("Disconnect", "Desconnectar"), - ("Enable file copy and paste", "Permetre copiar i enganxar arxius"), + ("Accept", "Accepta"), + ("Dismiss", "Ignora"), + ("Disconnect", "Desconnecta"), + ("Enable file copy and paste", "Habilita la còpia i enganxament de fitxers"), ("Connected", "Connectat"), - ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introduixi l'ID remot"), - ("Enter your password", "Introdueixi la seva contrasenya"), - ("Logging in...", "Iniciant sessió..."), - ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), + ("Direct and encrypted connection", "Connexió xifrada directa"), + ("Relayed and encrypted connection", "Connexió xifrada per repetidor"), + ("Direct and unencrypted connection", "Connexió directa sense xifratge"), + ("Relayed and unencrypted connection", "Connexió per repetidor sense xifratge"), + ("Enter Remote ID", "Inseriu la ID remota"), + ("Enter your password", "Inseriu la contrasenya"), + ("Logging in...", "S'està iniciant..."), + ("Enable RDP session sharing", "Habilita l'ús compartit de sessions RDP"), ("Auto Login", "Inici de sessió automàtic"), - ("Enable direct IP access", "Habilitar accés IP directe"), - ("Rename", "Renombrar"), + ("Enable direct IP access", "Habilita l'accés directe per IP"), + ("Rename", "Reanomena"), ("Space", "Espai"), - ("Create desktop shortcut", "Crear accés directe a l'escriptori"), - ("Change Path", "Cnviar ruta"), - ("Create Folder", "Crear carpeta"), - ("Please enter the folder name", "Indiqui el nom de la carpeta"), - ("Fix it", "Soluciona-ho"), - ("Warning", "Avís"), - ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), + ("Create desktop shortcut", "Crea una drecera a l'escriptori"), + ("Change Path", "Canvia la ruta"), + ("Create Folder", "Carpeta nova"), + ("Please enter the folder name", "Inseriu el nom de la carpeta"), + ("Fix it", "Repara"), + ("Warning", "Atenció"), + ("Login screen using Wayland is not supported", "L'inici de sessió amb Wayland encara no és compatible"), ("Reboot required", "Cal reiniciar"), ("Unsupported display server", "Servidor de visualització no compatible"), ("x11 expected", "x11 necessari"), ("Port", "Port"), - ("Settings", "Ajustaments"), - ("Username", " Nom d'usuari"), - ("Invalid port", "Port incorrecte"), - ("Closed manually by the peer", "Tancat manualment pel peer"), - ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), - ("Run without install", "Executar sense instal·lar"), - ("Connect via relay", "Connecta per relay"), - ("Always connect via relay", "Connecta sempre a través de relay"), - ("whitelist_tip", ""), - ("Login", "Inicia sessió"), - ("Verify", "Verificar"), + ("Settings", "Configuració"), + ("Username", "Nom d'usuari"), + ("Invalid port", "Port no vàlid"), + ("Closed manually by the peer", "Tancat manualment pel client"), + ("Enable remote configuration modification", "Habilita la modificació remota de la configuració"), + ("Run without install", "Inicia sense instal·lar"), + ("Connect via relay", "Connecta mitjançant un repetidor"), + ("Always connect via relay", "Connecta sempre mitjançant un repetidor"), + ("whitelist_tip", "Només les IP admeses es podran connectar"), + ("Login", "Inicia la sessió"), + ("Verify", "Verifica"), ("Remember me", "Recorda'm"), ("Trust this device", "Confia en aquest dispositiu"), ("Verification code", "Codi de verificació"), - ("verification_tip", ""), - ("Logout", "Sortir"), + ("verification_tip", "S'ha enviat un codi de verificació al correu-e registrat. Inseriu-lo per a continuar amb l'inici de sessió."), + ("Logout", "Tanca la sessió"), ("Tags", "Etiquetes"), - ("Search ID", "Cerca ID"), - ("whitelist_sep", ""), - ("Add ID", "Afegir ID"), - ("Add Tag", "Afegir tag"), - ("Unselect all tags", "Deseleccionar tots els tags"), - ("Network error", "Error de xarxa"), - ("Username missed", "Nom d'usuari oblidat"), - ("Password missed", "Contrasenya oblidada"), - ("Wrong credentials", "Credencials incorrectes"), - ("The verification code is incorrect or has expired", "El codi de verificació es incorrecte o ha expirat"), - ("Edit Tag", "Editar tag"), + ("Search ID", "Cerca per ID"), + ("whitelist_sep", "Separades per coma, punt i coma, espai o una adreça per línia"), + ("Add ID", "Afegeix una ID"), + ("Add Tag", "Afegeix una etiqueta"), + ("Unselect all tags", "Desselecciona totes les etiquetes"), + ("Network error", "Error de la xarxa"), + ("Username missed", "No s'ha indicat el nom d'usuari"), + ("Password missed", "No s'ha indicat la contrasenya"), + ("Wrong credentials", "Credencials errònies"), + ("The verification code is incorrect or has expired", "El codi de verificació no és vàlid o ha caducat"), + ("Edit Tag", "Edita l'etiqueta"), ("Forget Password", "Contrasenya oblidada"), ("Favorites", "Preferits"), - ("Add to Favorites", "Afegir a preferits"), - ("Remove from Favorites", "Treure de preferits"), - ("Empty", "Buit"), - ("Invalid folder name", "Nom de carpeta incorrecte"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Add to Favorites", "Afegeix als preferits"), + ("Remove from Favorites", "Suprimeix dels preferits"), + ("Empty", "Buida"), + ("Invalid folder name", "Nom de carpeta no vàlid"), + ("Socks5 Proxy", "Servidor intermediari Socks5"), + ("Socks5/Http(s) Proxy", "Servidor intermediari Socks5/Http(s)"), ("Discovered", "Descobert"), - ("install_daemon_tip", ""), - ("Remote ID", "ID remot"), - ("Paste", "Enganxar"), - ("Paste here?", "Enganxar aquí?"), - ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), - ("Download new version", "Descarregar nova versió"), + ("install_daemon_tip", "Per a iniciar durant l'arrencada del sistema, heu d'instal·lar el servei."), + ("Remote ID", "ID remota"), + ("Paste", "Enganxa"), + ("Paste here?", "Voleu enganxar aquí?"), + ("Are you sure to close the connection?", "Segur que voleu finalitzar la connexió?"), + ("Download new version", "Baixa la versió nova"), ("Touch mode", "Mode tàctil"), ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toqui amb un dit"), - ("Left Mouse", "Ratolí esquerra"), - ("One-Long Tap", "Toc llarg"), - ("Two-Finger Tap", "Toqui amb dos dits"), + ("One-Finger Tap", "Toc amb un dit"), + ("Left Mouse", "Botó esquerre"), + ("One-Long Tap", "Toc prolongat"), + ("Two-Finger Tap", "Toc amb dos dits"), ("Right Mouse", "Botó dret"), - ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toqui dos cops i mogui"), - ("Mouse Drag", "Arrastri amb el ratolí"), - ("Three-Finger vertically", "Tres dits verticalment"), + ("One-Finger Move", "Moviment amb un dit"), + ("Double Tap & Move", "Toc doble i moveu"), + ("Mouse Drag", "Arrossega el ratolí"), + ("Three-Finger vertically", "Tres dits en vertical"), ("Mouse Wheel", "Roda del ratolí"), ("Two-Finger Move", "Moviment amb dos dits"), ("Canvas Move", "Moviment del llenç"), - ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Ampliar llenç"), - ("Reset canvas", "Reestablir llenç"), - ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Pinch to Zoom", "Pessic per escalar"), + ("Canvas Zoom", "escala del llenç"), + ("Reset canvas", "Reinici del llenç"), + ("No permission of file transfer", "Cap permís per a transferència de fitxers"), ("Note", "Nota"), - ("Connection", "connexió"), - ("Share Screen", "Compartir pantalla"), + ("Connection", "Connexió"), + ("Share Screen", "Compartició de pantalla"), ("Chat", "Xat"), ("Total", "Total"), - ("items", "ítems"), + ("items", "elements"), ("Selected", "Seleccionat"), ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control d'entrada"), ("Audio Capture", "Captura d'àudio"), - ("File Connection", "connexió d'arxius"), - ("Screen Connection", "connexió de pantalla"), - ("Do you accept?", "Acceptes?"), - ("Open System Setting", "Configuració del sistema obert"), - ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), - ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), - ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), - ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), - ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), - ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("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?"), + ("android_input_permission_tip1", "Per a controlar de forma remota el vostre dispositiu amb gestos o un ratolí, heu de permetre al RustDesk l'ús del servei «Accessibilitat»."), + ("android_input_permission_tip2", "A l'apartat Configuració del sistema de la pàgina següent, aneu a «Serveis baixats», i activeu el «RustDesk Input»."), + ("android_new_connection_tip", "S'ha rebut una petició nova per a controlar el vostre dispositiu."), + ("android_service_will_start_tip", "Activant «Gravació de pantalla» s'iniciarà automàticament el servei que permet a altres enviar sol·licituds de connexió cap al vostre dispositiu."), + ("android_stop_service_tip", "Tancant el servei finalitzaran automàticament les connexions en ús."), + ("android_version_audio_tip", "Aquesta versió d'Android no suporta la captura d'àudio. Actualitzeu a Android 10 o superior."), + ("android_start_service_tip", "Toqueu a «Inicia el servei» o activeu el permís «Captura de pantalla» per a iniciar el servei de compartició de pantalla."), + ("android_permission_may_not_change_tip", "Els permisos per a les connexions ja establertes poden no canviar, fins que no torneu a connectar."), ("Account", "Compte"), - ("Overwrite", "Sobreescriure"), - ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), - ("Quit", "Sortir"), + ("Overwrite", "Reemplaça"), + ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix. Voleu ometre o reemplaçar l'original?"), + ("Quit", "Surt"), ("Help", "Ajuda"), ("Failed", "Ha fallat"), - ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), + ("Succeeded", "Fet"), + ("Someone turns on privacy mode, exit", "S'ha activat el Mode privat; surt"), ("Unsupported", "No suportat"), - ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·li complements"), - ("Peer exit", "El peer ha sortit"), - ("Failed to turn off", "Error en apagar"), - ("Turned off", "Apagat"), + ("Peer denied", "Client denegat"), + ("Please install plugins", "Instal·leu els complements"), + ("Peer exit", "Finalitzat pel client"), + ("Failed to turn off", "Ha fallat en desactivar"), + ("Turned off", "Desactivat"), ("Language", "Idioma"), - ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), - ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", "Engegar en l'arrencada"), - ("Start the screen sharing service on boot, requires special permissions", "Engegar el servei de captura de pantalla en l'arrencada, requereix permisos especials"), - ("Connection not allowed", "Connexió no disponible"), + ("Keep RustDesk background service", "Manté el servei del RustDesk en rerefons"), + ("Ignore Battery Optimizations", "Ignora les optimitzacions de bateria"), + ("android_open_battery_optimizations_tip", "Si voleu desactivar aquesta característica, feu-ho des de la pàgina següent de configuració del RustDesk, utilitzant l'opció relativa a «Bateria»"), + ("Start on boot", "Inicia durant l'arrencada"), + ("Start the screen sharing service on boot, requires special permissions", "Per iniciar la compartició de pantalla durant l'arrencada del sistema, calen permisos especials"), + ("Connection not allowed", "Connexió no permesa"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), - ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitzar contrasenya permament"), - ("Use both passwords", "Utilitzar ambdues contrasenyas"), - ("Set permanent password", "Establir contrasenya permament"), - ("Enable remote restart", "Activar reinici remot"), - ("Restart remote device", "Reiniciar dispositiu"), - ("Are you sure you want to restart", "Està segur que vol reiniciar?"), - ("Restarting remote device", "Reiniciant dispositiu remot"), - ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), - ("Copied", "Copiat"), - ("Exit Fullscreen", "Sortir de la pantalla completa"), + ("Translate mode", "Mode traduït"), + ("Use permanent password", "Utilitza la contrasenya permanent"), + ("Use both passwords", "Utilitza totes dues opcions"), + ("Set permanent password", "Estableix la contrasenya permanent"), + ("Enable remote restart", "Habilita el reinici remot"), + ("Restart remote device", "Reinicia el dispositiu remot"), + ("Are you sure you want to restart", "Segur que voleu reiniciar"), + ("Restarting remote device", "Reinici del dispositiu remot"), + ("remote_restarting_tip", "S'està reiniciant el dispositiu remot. Tanqueu aquest missatge i torneu a connectar amb ell mitjançant la contrasenya, un cop estigui en línia."), + ("Copied", "S'ha copiat"), + ("Exit Fullscreen", "Surt de la pantalla completa"), ("Fullscreen", "Pantalla completa"), - ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Seleccionar monitor"), - ("Control Actions", "Accions de control"), + ("Mobile Actions", "Funcions mòbils"), + ("Select Monitor", "Selecció de monitor"), + ("Control Actions", "Control de funcions"), ("Display Settings", "Configuració de pantalla"), ("Ratio", "Relació"), - ("Image Quality", "Qualitat d'imatge"), - ("Scroll Style", "Estil de desplaçament"), - ("Show Toolbar", "Mostrar la barra d'eines"), - ("Hide Toolbar", "Amancar la barra d'eines"), + ("Image Quality", "Qualitat de la imatge"), + ("Scroll Style", "Tipus de desplaçament"), + ("Show Toolbar", "Mostra la barra d'eines"), + ("Hide Toolbar", "Amaga la barra d'eines"), ("Direct Connection", "Connexió directa"), - ("Relay Connection", "Connexió Relay"), + ("Relay Connection", "Connexió amb repetidor"), ("Secure Connection", "Connexió segura"), - ("Insecure Connection", "Connexió insegura"), + ("Insecure Connection", "Connexió no segura"), ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptativa"), ("General", "General"), ("Security", "Seguretat"), ("Theme", "Tema"), - ("Dark Theme", "Tema Fosc"), + ("Dark Theme", "Tema fosc"), ("Light Theme", "Tema clar"), ("Dark", "Fosc"), ("Light", "Clar"), - ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguretat"), - ("Enable audio", "Habilitar àudio"), - ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), + ("Follow System", "Utilitza la configuració del sistema"), + ("Enable hardware codec", "Habilita la codificació per maquinari"), + ("Unlock Security Settings", "Desbloca la configuració de seguretat"), + ("Enable audio", "Habilita l'àudio"), + ("Unlock Network Settings", "Desbloca la configuració de la xarxa"), ("Server", "Servidor"), - ("Direct IP Access", "Accés IP Directe"), - ("Proxy", "Proxy"), - ("Apply", "Aplicar"), - ("Disconnect all devices?", "Desconnectar tots els dispositius?"), - ("Clear", "Netejar"), + ("Direct IP Access", "Accés directe per IP"), + ("Proxy", "Servidor intermediari"), + ("Apply", "Aplica"), + ("Disconnect all devices?", "Voleu desconnectar tots els dispositius?"), + ("Clear", "Buida"), ("Audio Input Device", "Dispositiu d'entrada d'àudio"), - ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Use IP Whitelisting", "Utilitza un llistat d'IP admeses"), ("Network", "Xarxa"), - ("Pin Toolbar", "Fixa la barra d'eines"), - ("Unpin Toolbar", "Soltar la barra d'eines"), - ("Recording", "Gravant"), - ("Directory", "Directori"), - ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), - ("Change", "Canviar"), - ("Start session recording", "Començar gravació de sessió"), - ("Stop session recording", "Aturar gravació de sessió"), - ("Enable recording session", "Habilitar gravació de sessió"), - ("Enable LAN discovery", "Habilitar descobriment de LAN"), - ("Deny LAN discovery", "Denegar descobriment de LAN"), - ("Write a message", "Escriure un missatge"), - ("Prompt", "Consultar"), - ("Please wait for confirmation of UAC...", "Sisplau, espereu per confirmar l'UAC..."), - ("elevated_foreground_window_tip", ""), + ("Pin Toolbar", "Ancora a la barra d'eines"), + ("Unpin Toolbar", "Desancora de la barra d'eines"), + ("Recording", "Gravació"), + ("Directory", "Contactes"), + ("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"), + ("Automatically record outgoing sessions", ""), + ("Change", "Canvia"), + ("Start session recording", "Inicia la gravació de la sessió"), + ("Stop session recording", "Atura la gravació de la sessió"), + ("Enable recording session", "Habilita la gravació de la sessió"), + ("Enable LAN discovery", "Habilita el descobriment LAN"), + ("Deny LAN discovery", "Inhabilita el descobriment LAN"), + ("Write a message", "Escriviu un missatge"), + ("Prompt", "Sol·licitud"), + ("Please wait for confirmation of UAC...", "Espereu a la confirmació de l'UAC..."), + ("elevated_foreground_window_tip", "La finestra de connexió actual requereix permisos ampliats per a funcionar i, de forma temporal, no es pot utilitzar ni el teclat ni el ratolí. Demaneu a l'usuari remot que minimitzi la finestra actual, o bé que faci clic al botó Permisos ampliats de la finestra d'administració de la connexió. Per a evitar aquest problema en un futur, instal·leu el RustDesk al dispositiu remot."), ("Disconnected", "Desconnectat"), ("Other", "Altre"), - ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), - ("Keyboard Settings", "Ajustaments de teclat"), - ("Full Access", "Acces complet"), - ("Screen Share", "Compartir pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), - ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), - ("Show RustDesk", "Mostrar RustDesk"), - ("This PC", "Aquest PC"), + ("Confirm before closing multiple tabs", "Confirma abans de tancar diverses pestanyes alhora"), + ("Keyboard Settings", "Configuració del teclat"), + ("Full Access", "Accés complet"), + ("Screen Share", "Compartició de pantalla"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("JumpLink", "Marcador"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), + ("Show RustDesk", "Mostra el RustDesk"), + ("This PC", "Aquest equip"), ("or", "o"), - ("Continue with", "Continuar amb"), - ("Elevate", "Elevar"), - ("Zoom cursor", "Zoom del ratolí"), - ("Accept sessions via password", "Acceptar sessions via contrasenya"), - ("Accept sessions via click", "Acceptar sessions via clic"), - ("Accept sessions via both", "Acceptar sessions via les dues opcions"), - ("Please wait for the remote side to accept your session request...", "Sisplau, espereu que la part remota accepti la teva sol·licitud de sessió..."), + ("Continue with", "Continua amb"), + ("Elevate", "Permisos ampliats"), + ("Zoom cursor", "Escala del ratolí"), + ("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"), + ("Accept sessions via click", "Accepta les sessions expressament amb el ratolí"), + ("Accept sessions via both", "Accepta les sessions de totes dues formes"), + ("Please wait for the remote side to accept your session request...", "S'està esperant l'acceptació remota de la vostra connexió..."), ("One-time Password", "Contrasenya d'un sol ús"), - ("Use one-time password", "Fer ser una contrasenya d'un sol ús"), - ("One-time password length", "Caracters de la contrasenya d'un sol ús"), - ("Request access to your device", "Sol·licitar l'acces al vostre dispositiu"), - ("Hide connection management window", "Amagar la finestra de gestió de connexió"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", "Clic dret per seleccionar pestanyes"), - ("Skipped", "Pasat"), - ("Add to address book", "Afegir a la llibreta d'adreces"), + ("Use one-time password", "Utilitza una contrasenya d'un sol ús"), + ("One-time password length", "Mida de la contrasenya d'un sol ús"), + ("Request access to your device", "Ha demanat connectar al vostre dispositiu"), + ("Hide connection management window", "Amaga la finestra d'administració de la connexió"), + ("hide_cm_tip", "Permet amagar la finestra només en acceptar sessions entrants sempre que s'utilitzi una contrasenya permanent"), + ("wayland_experiment_tip", "El suport per a Wayland està en fase experimental; es recomana l'ús d'x11 si us cal accés de forma desatesa."), + ("Right click to select tabs", "Feu clic amb el botó dret per a seleccionar pestanyes"), + ("Skipped", "S'ha omès"), + ("Add to address book", "Afegeix a la llibreta d'adreces"), ("Group", "Grup"), - ("Search", "Cercar"), - ("Closed manually by web console", "Tancat manualment amb la consola web"), + ("Search", "Cerca"), + ("Closed manually by web console", "Tancat manualment per la consola web"), ("Local keyboard type", "Tipus de teclat local"), - ("Select local keyboard type", "Seleccionar el tipus de teclat local"), - ("software_render_tip", ""), - ("Always use software rendering", "Sempre fer servir renderització per software"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), + ("Select local keyboard type", "Seleccioneu el tipus de teclat local"), + ("software_render_tip", "Si utilitzeu una gràfica Nvidia a Linux i la connexió remota es tanca immediatament en connectar, canviar al controlador lliure «Nouveau» amb renderització per programari, pot ajudar a solucionar el problema. Es requerirà en aquest cas reiniciar l'aplicació."), + ("Always use software rendering", "Utilitza sempre la renderització de programari"), + ("config_input", "Per a poder controlar el dispositiu remotament amb el teclat, faciliteu al RustDesk els permisos d'entrada necessaris."), + ("config_microphone", "Per a poder parlar remotament, faciliteu al RustDesk els permisos de gravació d'àudio necessaris."), + ("request_elevation_tip", "També, la part remota pot concedir aquests permisos de forma manual."), ("Wait", "Espereu"), - ("Elevation Error", "Error de elevació"), - ("Ask the remote user for authentication", "Demana autenticació a l'usuari remot"), - ("Choose this if the remote account is administrator", "Seleccionar això si l'usuari remot es administrador"), - ("Transmit the username and password of administrator", "Transmet el nom d'usuari i la contrasenya de l'administrador"), - ("still_click_uac_tip", ""), - ("Request Elevation", "Demanar l'elevació"), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", "Elevació exitosa"), + ("Elevation Error", "Error de permisos"), + ("Ask the remote user for authentication", "Demaneu l'autenticació al client remot"), + ("Choose this if the remote account is administrator", "Trieu aquesta opció si el compte remot té permisos d'administrador"), + ("Transmit the username and password of administrator", "Indiqueu l'usuari i contrasenya de l'administrador"), + ("still_click_uac_tip", "Es requereix acceptació manual a la part remota de la finestra «UAC» del RustDesk en execució."), + ("Request Elevation", "Sol·licita els permisos"), + ("wait_accept_uac_tip", "Espereu fins que l'usuari remot accepti la finestra de diàleg de l'«UAC»."), + ("Elevate successfully", "S'han acceptat els permisos"), ("uppercase", "majúscula"), ("lowercase", "minúscula"), - ("digit", "dígit"), + ("digit", "número"), ("special character", "caràcter especial"), - ("length>=8", ""), - ("Weak", "Dèbil"), - ("Medium", "Media"), - ("Strong", "Forta"), - ("Switch Sides", "Canviar l'esquerra i la dreta"), - ("Please confirm if you want to share your desktop?", "Sisplau, confirmeu si voleu compartir el seu escriptori?"), + ("length>=8", "mida>=8"), + ("Weak", "Feble"), + ("Medium", "Acceptable"), + ("Strong", "Segura"), + ("Switch Sides", "Inverteix la connexió"), + ("Please confirm if you want to share your desktop?", "Realment voleu que es controli aquest equip?"), ("Display", "Pantalla"), - ("Default View Style", "Estil de visualització predeterminat"), - ("Default Scroll Style", "Estil de desplaçament predeterminat"), - ("Default Image Quality", "Qualitat d'imatge predeterminada"), - ("Default Codec", "Còdec predeterminat"), - ("Bitrate", "Ratio de bits"), - ("FPS", ""), - ("Auto", "Auto"), - ("Other Default Options", "Altres opcions predeterminades"), - ("Voice call", "Trucada de veu"), - ("Text chat", "Xat de text"), - ("Stop voice call", "Penjar la trucada de veu"), - ("relay_hint_tip", ""), - ("Reconnect", "Reconectar"), - ("Codec", "Còdec"), + ("Default View Style", "Estil de vista per defecte"), + ("Default Scroll Style", "Estil de desplaçament per defecte"), + ("Default Image Quality", "Qualitat de la imatge per defecte"), + ("Default Codec", "Còdec per defecte"), + ("Bitrate", "Taxa de bits"), + ("FPS", "FPS"), + ("Auto", "Automàtic"), + ("Other Default Options", "Altres opcions per defecte"), + ("Voice call", "Trucada"), + ("Text chat", "Xat"), + ("Stop voice call", "Penja la trucada"), + ("relay_hint_tip", "Quan no sigui possible la connexió directa, podeu provar mitjançant un repetidor. Addicionalment, si voleu que l'ús d'un repetidor sigui la primera opció per defecte, podeu afegir el sufix «/r» a la ID, o seleccionar l'opció «Connecta sempre mitjançant un repetidor» si ja existeix una fitxa amb aquesta ID a la pestanya de connexions recents."), + ("Reconnect", "Torna a connectar"), + ("Codec", "Còdec"), ("Resolution", "Resolució"), - ("No transfers in progress", "Sense transferències en curs"), - ("Set one-time password length", "Seleccionar la longitud de la contrasenya d'un sol ús"), - ("RDP Settings", "Configuració RDP"), - ("Sort by", "Ordenar per"), - ("New Connection", "Nova connexió"), - ("Restore", "Restaurar"), - ("Minimize", "Minimizar"), - ("Maximize", "Maximizar"), - ("Your Device", "El teu dispositiu"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", "p.ex.: admin"), - ("Empty Username", "Usuari buit"), + ("No transfers in progress", "Cap transferència iniciada"), + ("Set one-time password length", "Mida de la contrasenya d'un sol ús"), + ("RDP Settings", "Opcions de connexió RDP"), + ("Sort by", "Organitza per"), + ("New Connection", "Connexió nova"), + ("Restore", "Restaura"), + ("Minimize", "Minimitza"), + ("Maximize", "Maximitza"), + ("Your Device", "Aquest dispositiu"), + ("empty_recent_tip", "No s'ha trobat cap sessió recent!\nS'afegiran automàticament les connexions que realitzeu."), + ("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", "Jo"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", "Tipus de visualització"), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", "Sistema de so"), - ("Default", "Predeterminat"), - ("New RDP", "Nou RDP"), - ("Fingerprint", "Empremta digital"), - ("Copy Fingerprint", "Copiar l'emprenta digital"), - ("no fingerprints", "sense emprentes"), - ("Select a peer", "Seleccionar un peer"), - ("Select peers", "Seleccionar varios peers"), - ("Plugins", "Plugins"), - ("Uninstall", "Desinstal·lar"), - ("Update", "Actualitzar"), - ("Enable", "Activar"), - ("Disable", "Desactivar"), + ("Me", "Vós"), + ("identical_file_tip", "Aquest fitxer és idèntic al del client."), + ("show_monitors_tip", "Mostra les pantalles a la barra d'eines"), + ("View Mode", "Mode espectador"), + ("login_linux_tip", "És necessari que inicieu prèviament sessió amb un entorn d'escriptori x11 habilitat"), + ("verify_rustdesk_password_tip", "Verifica la contrasenya del RustDesk"), + ("remember_account_tip", "Recorda aquest compte"), + ("os_account_desk_tip", "S'utilitza aquest compte per iniciar la sessió al sistema remot i habilitar el mode sense cap pantalla connectada"), + ("OS Account", "Compte d'usuari"), + ("another_user_login_title_tip", "Altre usuari ha iniciat ja una sessió"), + ("another_user_login_text_tip", "Desconnecta"), + ("xorg_not_found_title_tip", "No s'ha trobat l'entorn Xorg"), + ("xorg_not_found_text_tip", "Instal·leu el Xorg"), + ("no_desktop_title_tip", "Cap escriptori disponible"), + ("no_desktop_text_tip", "Instal·leu l'entorn d'escriptori GNOME"), + ("No need to elevate", "No calen permisos ampliats"), + ("System Sound", "So del sistema"), + ("Default", "per defecte"), + ("New RDP", "Connexió RDP nova"), + ("Fingerprint", "Empremta"), + ("Copy Fingerprint", "Copia l'empremta"), + ("no fingerprints", "Cap empremta"), + ("Select a peer", "Seleccioneu un client"), + ("Select peers", "Seleccioneu els clients"), + ("Plugins", "Complements"), + ("Uninstall", "Desinstal·la"), + ("Update", "Actualitza"), + ("Enable", "Activa"), + ("Disable", "Desactiva"), ("Options", "Opcions"), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", "Col·lapsar la barra d'etiquetes"), - ("Accept and Elevate", "Aceptar i elevar"), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), + ("resolution_original_tip", "Resolució original"), + ("resolution_fit_local_tip", "Ajusta la resolució local"), + ("resolution_custom_tip", "Resolució personalitzada"), + ("Collapse toolbar", "Minimitza la barra d'eines"), + ("Accept and Elevate", "Accepta i permet"), + ("accept_and_elevate_btn_tooltip", "Accepta la connexió i permet els permisos elevats UAC."), + ("clipboard_wait_response_timeout_tip", "S'ha esgotat el temps d'espera amb la resposta de còpia."), ("Incoming connection", "Connexió entrant"), ("Outgoing connection", "Connexió sortint"), - ("Exit", "Tancar"), - ("Open", "Obrir"), - ("logout_tip", ""), - ("Service", "Servici"), - ("Start", "Iniciar"), - ("Stop", "Aturar"), - ("exceed_max_devices", ""), - ("Sync with recent sessions", "Sincronitzar amb les sessions recents"), - ("Sort tags", "Ordenar per etiquetes"), - ("Open connection in new tab", "Obrir connexió en una nova pestanya"), - ("Move tab to new window", "Mou la pestanya a una nova finestra"), + ("Exit", "Surt"), + ("Open", "Obre"), + ("logout_tip", "Segur que voleu desconnectar?"), + ("Service", "Servei"), + ("Start", "Inicia"), + ("Stop", "Atura"), + ("exceed_max_devices", "Heu assolit el nombre màxim de dispositius administrables."), + ("Sync with recent sessions", "Sincronitza amb les sessions recents"), + ("Sort tags", "Ordena les etiquetes"), + ("Open connection in new tab", "Obre la connexió en una pestanya nova"), + ("Move tab to new window", "Mou la pestanya a una finestra nova"), ("Can not be empty", "No pot estar buit"), ("Already exists", "Ja existeix"), - ("Change Password", "Canviar la contrasenya"), - ("Refresh Password", "Refrescar la contrasenya"), + ("Change Password", "Canvia la contrasenya"), + ("Refresh Password", "Actualitza la contrasenya"), ("ID", "ID"), - ("Grid View", "Visualització de grilla"), - ("List View", "Visualització de llista"), - ("Select", "Seleccionar"), - ("Toggle Tags", "Activar/desactivar etiquetes"), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", "Canviar el color"), - ("Primary Color", "Color primari"), + ("Grid View", "Disposició de graella"), + ("List View", "Disposició de llista"), + ("Select", "Selecciona"), + ("Toggle Tags", "Habilita les etiquetes"), + ("pull_ab_failed_tip", "Ha fallat en actualitzar la llista de contactes"), + ("push_ab_failed_tip", "Ha fallat en actualitzar la llista amb el servidor"), + ("synced_peer_readded_tip", "Els dispositius que es troben a la llista de sessions recents se sincronitzaran novament a la llista de contactes."), + ("Change Color", "Canvia el color"), + ("Primary Color", "Color principal"), ("HSV Color", "Color HSV"), - ("Installation Successful!", "Instal·lació correcta!"), - ("Installation failed!", "Instal·lació fallada!"), - ("Reverse mouse wheel", "Canviar la orientació de la roda del ratolí"), + ("Installation Successful!", "S'ha instal·lat correctament"), + ("Installation failed!", "Ha fallat la instal·lació"), + ("Reverse mouse wheel", "Inverteix la roda del ratolí"), ("{} sessions", "{} sessions"), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", "No mostrar més"), - ("I Agree", "D'acord"), - ("Decline", "Rebutjar"), - ("Timeout in minutes", "Desconexió en minuts"), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", "Connexió fallada per inactivitat"), - ("Check for software update on startup", "Revisar actualitzacions de software al iniciar"), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", "Filtrar per intersecció"), - ("Remove wallpaper during incoming sessions", "Amagar el fons de pantalla en les connexions entrants"), + ("scam_title", "Podríeu ser víctima d'una ESTAFA!"), + ("scam_text1", "Si cap persona qui NO coneixeu NI CONFIEU us demanés l'ús del RustDesk, no continueu i talleu la comunicació immediatament."), + ("scam_text2", "Habitualment solen ser atacants intentant fer-se amb els vostres diners o informació privada."), + ("Don't show again", "No tornis a mostrar"), + ("I Agree", "Accepto"), + ("Decline", "No accepto"), + ("Timeout in minutes", "Temps d'espera en minuts"), + ("auto_disconnect_option_tip", "Tanca automàticament les sessions entrants per inactivitat de l'usuari"), + ("Connection failed due to inactivity", "Ha fallat la connexió per inactivitat"), + ("Check for software update on startup", "Cerca actualitzacions en iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Actualitzeu el RustDesk Server Pro a la versió {} o superior!"), + ("pull_group_failed_tip", "Ha fallat en actualitzar el grup"), + ("Filter by intersection", "Filtra per intersecció"), + ("Remove wallpaper during incoming sessions", "Inhabilita el fons d'escriptori durant la sessió entrant"), ("Test", "Prova"), - ("display_is_plugged_out_msg", ""), - ("No displays", "Sense pantalles"), - ("Open in new window", "Obrir en una nova finestra"), - ("Show displays as individual windows", "Mostrar pantalles com finestres individuals"), - ("Use all my displays for the remote session", "Fer servir totes las meves pantalles per la sessió remota"), - ("selinux_tip", ""), - ("Change view", "Canviar la vista"), - ("Big tiles", "Títols grans"), - ("Small tiles", "Títols petits"), + ("display_is_plugged_out_msg", "El monitor està desconnectat; canvieu primer al monitor principal."), + ("No displays", "Cap monitor"), + ("Open in new window", "Obre en una finestra nova"), + ("Show displays as individual windows", "Mostra cada monitor com una finestra individual"), + ("Use all my displays for the remote session", "Utilitza tots els meus monitors per a la connexió remota"), + ("selinux_tip", "SELinux està activat al vostre dispositiu, la qual cosa evita que el RustDesk funcioni correctament com a equip controlable."), + ("Change view", "Canvia la vista"), + ("Big tiles", "Mosaic gran"), + ("Small tiles", "Mosaic petit"), ("List", "Llista"), ("Virtual display", "Pantalla virtual"), - ("Plug out all", "Desconnectar tots"), + ("Plug out all", "Desconnecta-ho tot"), ("True color (4:4:4)", "Color real (4:4:4)"), - ("Enable blocking user input", "Activar bloqueig d'entrada d'usuari"), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", "Activar el mode de privacitat"), - ("Exit privacy mode", "Sortir del mode de privacitat"), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", "Canviar la tecla de control"), - ("swap-left-right-mouse", ""), + ("Enable blocking user input", "Bloca el control de l'usuari amb els dispositius d'entrada"), + ("id_input_tip", "Evita que l'usuari pugui interactuar p. ex. amb el teclat o ratolí"), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Inicia el Mode privat"), + ("Exit privacy mode", "Surt del Mode privat"), + ("idd_not_support_under_win10_2004_tip", "El controlador indirecte de pantalla no està suportat; es requereix Windows 10 versió 2004 o superior."), + ("input_source_1_tip", "Font d'entrada 1"), + ("input_source_2_tip", "Font d'entrada 2"), + ("Swap control-command key", "Canvia el comportament de la tecla Control"), + ("swap-left-right-mouse", "Alterna el comportament dels botons esquerre-dret del ratolí"), ("2FA code", "Codi 2FA"), ("More", "Més"), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", "El codi de verificació de l'email ha de tenir 6 caràcters."), - ("2FA code must be 6 digits.", "El codi 2FA ha de tenir 6 digits."), - ("Multiple Windows sessions found", "Multiples sessions de Windows trobades"), - ("Please select the session you want to connect to", "Sisplau, seleccioni la sessió que voleu connectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), + ("enable-2fa-title", "Habilita el mètode d'autenticació de factor doble"), + ("enable-2fa-desc", "Configureu ara el vostre autenticador. Podeu utilitzar una aplicació com 2fast, FreeOTP, MultiOTP, Microsoft o Google Authenticator al vostre telèfon o escriptori.\n\nEscanegeu el codi QR amb l'aplicació i escriviu els caràcters resultants per habilitar l'autenticació de factor doble."), + ("wrong-2fa-code", "Codi 2FA no vàlid. Verifiqueu el que heu escrit i també que la configuració horària sigui correcta"), + ("enter-2fa-title", "Autenticació de factor doble"), + ("Email verification code must be 6 characters.", "El codi de verificació de correu-e són 6 caràcters"), + ("2FA code must be 6 digits.", "El codi de verificació 2FA haurien de ser almenys 6 dígits"), + ("Multiple Windows sessions found", "S'han trobat múltiples sessions en ús del Windows"), + ("Please select the session you want to connect to", "Indiqueu amb quina sessió voleu connectar"), + ("powered_by_me", "Amb la tecnologia de RustDesk"), + ("outgoing_only_desk_tip", "Aquesta és una versió personalitzada.\nPodeu connectar amb altres dispositius, però no s'accepten connexions d'entrada cap el vostre dispositiu."), + ("preset_password_warning", "Aquesta versió personalitzada té una contrasenya preestablerta. Qualsevol persona que la conegui pot tenir accés total al vostre dispositiu. Si no és el comportament desitjat, desinstal·leu aquest programa immediatament."), ("Security Alert", "Alerta de seguretat"), - ("My address book", "La meva llibreta d'adreces"), + ("My address book", "Llibreta d'adreces"), ("Personal", "Personal"), ("Owner", "Propietari"), - ("Set shared password", "Establir la contrasenya compartida"), - ("Exist in", "Existeix en"), + ("Set shared password", "Establiu una contrasenya compartida"), + ("Exist in", "Existeix a"), ("Read-only", "Només lectura"), ("Read/Write", "Lectura/Escriptura"), ("Full Control", "Control total"), - ("share_warning_tip", ""), - ("Everyone", "Tots"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", "Segueix el cursor remot"), - ("Follow remote window focus", "Segueix el focus de la finestra remota"), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("share_warning_tip", "Els camps a continuació estan compartits i són visibles a d'altres."), + ("Everyone", "Tothom"), + ("ab_web_console_tip", "Més a la consola web"), + ("allow-only-conn-window-open-tip", "Permet la connexió només si la finestra del RustDesk està activa"), + ("no_need_privacy_mode_no_physical_displays_tip", "Cap monitor físic. No cal l'ús del Mode privat"), + ("Follow remote cursor", "Segueix al cursor remot"), + ("Follow remote window focus", "Segueix el focus remot de la finestra activa"), + ("default_proxy_tip", "El protocol per defecte és Socks5 al port 1080"), + ("no_audio_input_device_tip", "No s'ha trobat cap dispositiu d'àudio."), ("Incoming", "Entrant"), ("Outgoing", "Sortint"), - ("Clear Wayland screen selection", "Netejar la selecció de pantalla Wayland"), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", "Fer servir renderització de textures"), + ("Clear Wayland screen selection", "Neteja la pantalla de selecció Wayland"), + ("clear_Wayland_screen_selection_tip", "En netejar la finestra de selecció, podreu tornar a triar quina pantalla compartir."), + ("confirm_clear_Wayland_screen_selection_tip", "Segur que voleu netejar la pantalla de selecció del Wayland"), + ("android_new_voice_call_tip", "S'ha rebut una petició de trucada entrant. Si accepteu, la font d'àudio canviarà a comunicació per veu."), + ("texture_render_tip", "Utilitzeu aquesta opció per suavitzar la imatge. Desactiveu-ho si trobeu cap problema amb el renderitzat"), + ("Use texture rendering", "Utilitza la renderització de textures"), ("Floating window", "Finestra flotant"), - ("floating_window_tip", ""), - ("Keep screen on", "Mantenir la pantalla encesa"), + ("floating_window_tip", "Ajuda a mantenir el servei del RustDesk en rerefons"), + ("Keep screen on", "Manté la pantalla activa"), ("Never", "Mai"), - ("During controlled", "Mentre estigui controlat"), - ("During service is on", "Mentres el servei estigui encés"), - ("Capture screen using DirectX", "Captura de pantalla utilitzant DirectX"), + ("During controlled", "Durant la connexió"), + ("During service is on", "Mentre el servei està actiu"), + ("Capture screen using DirectX", "Captura utilitzant el DirectX"), ("Back", "Enrere"), ("Apps", "Aplicacions"), - ("Volume up", "Pujar el volum"), - ("Volume down", "Baixa el volum"), - ("Power", "Engegar"), - ("Telegram bot", "Bot de Telegram"), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("Volume up", "Volum amunt"), + ("Volume down", "Volum avall"), + ("Power", "Encesa"), + ("Telegram bot", "Bot del Telegram"), + ("enable-bot-tip", "Si habiliteu aquesta característica, podreu rebre el codi 2FA mitjançant el vostre bot. També funciona com a notificador de la connexió."), + ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"), + ("cancel-2fa-confirm-tip", "Segur que voleu cancel·lar l'autenticació 2FA?"), + ("cancel-bot-confirm-tip", "Segur que voleu cancel·lar el bot de Telegram?"), + ("About RustDesk", "Quant al RustDesk"), + ("Send clipboard keystrokes", "Envia les pulsacions de tecles del porta-retalls"), + ("network_error_tip", "Verifiqueu la vostra connexió a Internet i torneu a provar"), + ("Unlock with PIN", "Desbloca amb PIN"), + ("Requires at least {} characters", "Són necessaris almenys {} caràcters"), + ("Wrong PIN", "PIN no vàlid"), + ("Set PIN", "Definiu un codi PIN"), + ("Enable trusted devices", "Habilita els dispositius de confiança"), + ("Manage trusted devices", "Administra els dispositius de confiança"), + ("Platform", "Platforma"), + ("Days remaining", "Dies restants"), + ("enable-trusted-devices-tip", "Omet l'autenticació de factor doble (2FA) als dispositius de confiança"), + ("Parent directory", "Carpeta pare"), + ("Resume", "Continua"), + ("Invalid file name", "Nom de fitxer no vàlid"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("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", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index df9fb5303212..18bc5e6381ed 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), - ("Slogan_tip", ""), + ("Slogan_tip", "在这个混乱的世界中,用心制作!"), ("Privacy Statement", "隐私声明"), ("Mute", "静音"), ("Build Date", "构建日期"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "显示质量监测"), ("Disable clipboard", "禁用剪贴板"), ("Lock after session end", "会话结束后锁定远程电脑"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), ("ID does not exist", "ID 不存在"), @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1 传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -346,7 +346,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", "解锁网络设置"), @@ -363,7 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "取消固定工具栏"), ("Recording", "录屏"), ("Directory", "目录"), - ("Automatically record incoming sessions", "自动录制来访会话"), + ("Automatically record incoming sessions", "自动录制传入会话"), + ("Automatically record outgoing sessions", "自动录制传出会话"), ("Change", "更改"), ("Start session recording", "开始录屏"), ("Stop session recording", "结束录屏"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "真彩模式(4:4:4)"), ("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\"。"), + ("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", "进入隐私模式"), @@ -624,12 +625,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "提升音量"), ("Volume down", "降低音量"), ("Power", "电源"), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), + ("Telegram bot", "Telegram 机器人"), + ("enable-bot-tip", "如果您启用此功能,您可以从您的机器人接收双重认证码,亦可作为连线通知之用。"), + ("enable-bot-desc", "1. 开启与 @BotFather 的对话。\n2. 发送命令 \"/newbot\"。 您将在完成此步骤后收到权杖 (Token)。\n3. 开始与您刚创建的机器人的对话。发送一则以正斜杠 (\"/\") 开头的消息来启用它,例如 \"/hello\"。"), ("cancel-2fa-confirm-tip", "确定要取消双重认证吗?"), ("cancel-bot-confirm-tip", "确定要取消 Telegram 机器人吗?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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,web 客户端不支持直接 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", "{} 版本更新"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a85a4d560b81..e36c493618bf 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit ID"), - ("Your new ID", "Váše nové ID"), + ("Your new ID", "Vaše nové ID"), ("length %min% to %max%", "délka mezi %min% a %max%"), ("starts with a letter", "začíná písmenem"), ("allowed characters", "povolené znaky"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobrazit monitor kvality"), ("Disable clipboard", "Vypnout schránku"), ("Lock after session end", "Po ukončení relace zamknout plochu"), - ("Insert", "Vložit"), + ("Insert Ctrl + Alt + Del", "Vložit Ctrl + Alt + Del"), ("Insert Lock", "Zamknout"), ("Refresh", "Načíst znovu"), ("ID does not exist", "Toto ID neexistuje"), @@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create desktop icon", "Vytvořit ikonu na ploše"), ("agreement_tip", "Spuštěním instalace přijímáte licenční ujednání."), ("Accept and Install", "Přijmout a nainstalovat"), - ("End-user license agreement", "Licencenční ujednání s koncovým uživatelem"), + ("End-user license agreement", "Licenční ujednání s koncovým uživatelem"), ("Generating ...", "Vytváření ..."), ("Your installation is lower version.", "Máte nainstalovanou starší verzi"), ("not_close_tcp_tip", "Po dobu, po kterou tunel potřebujete, nezavírejte toto okno"), @@ -263,7 +263,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Posun zobrazení"), ("Pinch to Zoom", "Přiblížíte roztažením dvěma prsty"), ("Canvas Zoom", "Přiblížení zobrazení"), - ("Reset canvas", "Vrátit měřtko zobrazení na výchozí"), + ("Reset canvas", "Vrátit měřítko zobrazení na výchozí"), ("No permission of file transfer", "Žádné oprávnění k přenosu souborů"), ("Note", "Poznámka"), ("Connection", "Připojení"), @@ -297,7 +297,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Úspěšný"), ("Someone turns on privacy mode, exit", "Někdo zapne režim ochrany soukromí, ukončete ho"), ("Unsupported", "Nepodporováno"), - ("Peer denied", "Protistana odmítnula"), + ("Peer denied", "Protistrana odmítla"), ("Please install plugins", "Nainstalujte si prosím pluginy"), ("Peer exit", "Ukončení protistrany"), ("Failed to turn off", "Nepodařilo se vypnout"), @@ -329,7 +329,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "Nastavení obrazovky"), ("Ratio", "Poměr"), ("Image Quality", "Kvalita obrazu"), - ("Scroll Style", "Štýl posúvania"), + ("Scroll Style", "Styl posouvání"), ("Show Toolbar", "Zobrazit panel nástrojů"), ("Hide Toolbar", "Skrýt panel nástrojů"), ("Direct Connection", "Přímé spojení"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávání"), ("Directory", "Adresář"), ("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"), + ("Automatically record outgoing sessions", ""), ("Change", "Změnit"), ("Start session recording", "Spustit záznam relace"), ("Stop session recording", "Zastavit záznam relace"), @@ -458,7 +459,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimalizovat"), ("Maximize", "Maximalizovat"), ("Your Device", "Vaše zařízení"), - ("empty_recent_tip", "Ups, žádná nedávná relace!\nČas naplánovat nové."), + ("empty_recent_tip", "Jejda, žádná nedávná relace!\nČas naplánovat novou."), ("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é."), @@ -586,7 +587,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "Poháněno společností RustDesk"), ("outgoing_only_desk_tip", "Toto je přizpůsobená edice.\nMůžete se připojit k jiným zařízením, ale jiná zařízení se k vašemu zařízení připojit nemohou."), ("preset_password_warning", "Tato upravená edice je dodávána s přednastaveným heslem. Každý, kdo zná toto heslo, může získat plnou kontrolu nad vaším zařízením. Pokud jste to nečekali, okamžitě software odinstalujte."), - ("Security Alert", "Bezbečnostní Výstraha"), + ("Security Alert", "Bezpečnostní výstraha"), ("My address book", "Můj adresář"), ("Personal", "Osobní"), ("Owner", "Majitel"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Otevřete chat s @BotFather.\n2, Pošlete příkaz \"/newbot\". Po dokončení tohoto kroku obdržíte token.\n3, Spusťte chat s nově vytvořeným botem. Pro jeho aktivaci odešlete zprávu začínající lomítkem vpřed (\"/\"), například \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Jste si jisti, že chcete zrušit 2FA?"), ("cancel-bot-confirm-tip", "Jste si jisti, že chcete zrušit bota Telegramu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odesílat stisky kláves schránky"), + ("network_error_tip", "Zkontrolujte prosím připojení k síti a klikněte na tlačítko Opakovat."), + ("Unlock with PIN", "Odemknout PINem"), + ("Requires at least {} characters", "Vyžadováno aspoň {} znaků"), + ("Wrong PIN", "Nesprávný PIN"), + ("Set PIN", "Nastavit PIN"), + ("Enable trusted devices", "Povolit důvěryhodná zařízení"), + ("Manage trusted devices", "Spravovat důvěryhodná zařízení"), + ("Platform", "Platforma"), + ("Days remaining", "Zbývajících dnů"), + ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), + ("Parent directory", "Rodičovský adresář"), + ("Resume", "Pokračovat"), + ("Invalid file name", "Nesprávný název souboru"), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 679394599e5b..7988da242e74 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Dit skrivebord"), - ("desk_tip", "Du kan få adgang til dit skrivebord med dette ID og adgangskode."), + ("desk_tip", "Du kan give adgang til dit skrivebord med dette ID og denne adgangskode."), ("Password", "Adgangskode"), ("Ready", "Klar"), ("Established", "Etableret"), @@ -38,18 +38,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændr ID"), ("Your new ID", "Dit nye ID"), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), + ("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."), ("Website", "Hjemmeside"), ("About", "Om"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"), + ("Privacy Statement", "Privatlivspolitik"), ("Mute", "Sluk for mikrofonen"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Build dato"), + ("Version", "Version"), + ("Home", "Hjem"), ("Audio Input", "Lydinput"), ("Enhancements", "Forbedringer"), ("Hardware Codec", "Hardware-codec"), @@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Stræk ud"), - ("Scrollbar", "Rullebar"), - ("ScrollAuto", "Auto-rul"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "Auto-scroll"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis billedkvalitet"), ("Disable clipboard", "Deaktiver udklipsholder"), ("Lock after session end", "Lås efter afslutningen af fjernstyring"), - ("Insert", "Indsæt"), + ("Insert Ctrl + Alt + Del", "Indsæt Ctrl + Alt + Del"), ("Insert Lock", "Indsæt lås"), ("Refresh", "Genopfrisk"), ("ID does not exist", "ID findes ikke"), @@ -139,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Fjernskrivebord er offline"), ("Key mismatch", "Nøgle uoverensstemmelse"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Forbindelse til relæ-serveren mislykkedes"), + ("Failed to connect to relay server", "Forbindelse til relay-serveren mislykkedes"), ("Failed to connect via rendezvous server", "Forbindelse via Rendezvous-server mislykkedes"), - ("Failed to connect via relay server", "Forbindelse via relæ-serveren mislykkedes"), + ("Failed to connect via relay server", "Forbindelse via relay-serveren mislykkedes"), ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"), ("Set Password", "Indstil adgangskode"), ("OS Password", "Operativsystemadgangskode"), @@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember me", "Husk mig"), ("Trust this device", "Husk denne enhed"), ("Verification code", "Verifikationskode"), - ("verification_tip", ""), + ("verification_tip", "En bekræftelseskode er blevet sendt til den registrerede e-mail adresse. Indtast bekræftelseskoden for at logge på."), ("Logout", "Logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg efter ID"), @@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Glemt brugernavn"), ("Password missed", "Glemt kodeord"), ("Wrong credentials", "Forkerte registreringsdata"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Bekræftelsesnøglen er forkert eller er udløbet"), ("Edit Tag", "Rediger nøgleord"), ("Forget Password", "Glem adgangskoden"), ("Favorites", "Favoritter"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"), ("Download new version", "Download ny version"), ("Touch mode", "Touch-tilstand"), - ("Mouse mode", "Musse-tilstand"), + ("Mouse mode", "Muse-tilstand"), ("One-Finger Tap", "En-finger-tryk"), ("Left Mouse", "Venstre mus"), ("One-Long Tap", "Tryk og hold med en finger"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."), ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), ("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."), + ("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."), ("Account", "Konto"), ("Overwrite", "Overskriv"), ("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), @@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Sprog"), ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Hvis du ønsker at slukke for denne funktion, åbn RustDesk appens indstillinger, tryk på [Batteri], og fjern flueben ved [Uden begrænsninger]"), ("Start on boot", "Start under opstart"), ("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), ("Connection not allowed", "Forbindelse ikke tilladt"), @@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Map mode", "Kortmodus"), ("Translate mode", "Oversættelsesmodus"), ("Use permanent password", "Brug permanent adgangskode"), - ("Use both passwords", "Brug begge adgangskoder"), + ("Use both passwords", "Brug begge typer adgangskoder"), ("Set permanent password", "Sæt permanent adgangskode"), ("Enable remote restart", "Aktivér fjerngenstart"), ("Restart remote device", "Genstart fjernenhed"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Forhold"), ("Image Quality", "Billedkvalitet"), ("Scroll Style", "Rullestil"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Vis værktøjslinje"), + ("Hide Toolbar", "Skjul værktøjslinje"), ("Direct Connection", "Direkte forbindelse"), ("Relay Connection", "Viderestillingsforbindelse"), ("Secure Connection", "Sikker forbindelse"), @@ -359,24 +359,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Lydindgangsenhed"), ("Use IP Whitelisting", "Brug IP Whitelisting"), ("Network", "Netværk"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Fastgør værktøjslinjen"), + ("Unpin Toolbar", "Frigiv værktøjslinjen"), ("Recording", "Optager"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), + ("Automatically record outgoing sessions", ""), ("Change", "Ændr"), ("Start session recording", "Start sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"), ("Enable recording session", "Aktivér optagelsessession"), - ("Enable LAN discovery", "Aktivér LAN Discovery"), - ("Deny LAN discovery", "Afvis LAN Discovery"), + ("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"), + ("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"), ("Write a message", "Skriv en besked"), ("Prompt", "Prompt"), ("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "Det nuværende vindue på fjernskrivebordet kræver højere rettigheder for at køre, så det er midlertidigt ikke muligt at bruge musen og tastaturet. Du kan bede fjernbrugeren om at minimere vinduet, eller trykke på elevér knappen i forbindelsesvinduet. For at undgå dette problem, er det anbefalet at installere RustDesk på fjernenheden."), ("Disconnected", "Afbrudt"), ("Other", "Andre"), - ("Confirm before closing multiple tabs", "Bekræft før du lukker flere faner"), + ("Confirm before closing multiple tabs", "Bekræft nedlukning hvis der er flere faner"), ("Keyboard Settings", "Tastaturindstillinger"), ("Full Access", "Fuld adgang"), ("Screen Share", "Skærmdeling"), @@ -399,8 +400,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Engangskode længde"), ("Request access to your device", "Efterspørg adgang til din enhed"), ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), + ("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"), + ("wayland_experiment_tip", "Wayland understøttelse er stadigvæk under udvikling. Hvis du har brug for ubemandet adgang, bedes du bruge X11."), ("Right click to select tabs", "Højreklik for at vælge faner"), ("Skipped", "Sprunget over"), ("Add to address book", "Tilføj til adressebog"), @@ -409,19 +410,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), ("Local keyboard type", "Lokal tastatur type"), ("Select local keyboard type", "Vælg lokal tastatur type"), - ("software_render_tip", ""), + ("software_render_tip", "Hvis du bruger et Nvidia grafikkort på Linux, og fjernskrivebordsvinduet lukker ned med det samme efter forbindelsen er oprettet, kan det hjælpe at skifte til Nouveau open-source driveren, og aktivere software rendering. Et genstart af RustDesk er nødvendigt."), ("Always use software rendering", "Brug altid software rendering"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), + ("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"), + ("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"), + ("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."), ("Wait", "Vent"), ("Elevation Error", "Elevationsfejl"), ("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), ("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"), ("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"), - ("still_click_uac_tip", ""), + ("still_click_uac_tip", "Kræver stadigvæk at fjernbrugeren skal trykke OK på UAC vinduet ved kørsel af RustDesk."), ("Request Elevation", "Efterspørger elevation"), - ("wait_accept_uac_tip", ""), + ("wait_accept_uac_tip", "Vent venligst på at fjernbrugeren accepterer UAC dialog forespørgslen."), ("Elevate successfully", "Elevation lykkedes"), ("uppercase", "store bogstaver"), ("lowercase", "små bogstaver"), @@ -435,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"), ("Display", "Visning"), ("Default View Style", "Standard visningsstil"), - ("Default Scroll Style", "Standard rulle stil"), + ("Default Scroll Style", "Standard scrollestil"), ("Default Image Quality", "Standard billedkvalitet"), ("Default Codec", "Standard codec"), ("Bitrate", "Bitrate"), @@ -445,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Stemmeopkald"), ("Text chat", "Tekstchat"), ("Stop voice call", "Stop stemmeopkald"), - ("relay_hint_tip", ""), + ("relay_hint_tip", "Det kan ske, at det ikke er muligt at forbinde direkte; du kan forsøge at forbinde via en relay-server. Derudover, hvis du ønsker at bruge en relay-server på dit første forsøg, kan du tilføje \"/r\" efter ID'et, eller bruge valgmuligheden \"Forbind altid via relay-server\" i fanen for seneste sessioner, hvis den findes."), ("Reconnect", "Genopret"), ("Codec", "Codec"), ("Resolution", "Opløsning"), @@ -458,178 +459,202 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimér"), ("Maximize", "Maksimér"), ("Your Device", "Din enhed"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), + ("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."), + ("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"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New 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", ""), - ("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", ""), + ("identical_file_tip", "Denne fil er identisk med modpartens."), + ("show_monitors_tip", "Vis skærme i værktøjsbjælken"), + ("View Mode", "Visningstilstand"), + ("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"), + ("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"), + ("remember_account_tip", "Husk denne konto"), + ("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"), + ("OS Account", "Styresystem konto"), + ("another_user_login_title_tip", "En anden bruger er allerede logget ind"), + ("another_user_login_text_tip", "Frakobl"), + ("xorg_not_found_title_tip", "Xorg ikke fundet"), + ("xorg_not_found_text_tip", "Installér venlist Xorg"), + ("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"), + ("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"), + ("No need to elevate", "Ingen grund til at elevere"), + ("System Sound", "Systemlyd"), + ("Default", "Standard"), + ("New RDP", "Ny RDP"), + ("Fingerprint", "Fingeraftryk"), + ("Copy Fingerprint", "Kopiér fingeraftryk"), + ("no fingerprints", "Ingen fingeraftryk"), + ("Select a peer", "Vælg en peer"), + ("Select peers", "Vælg peers"), + ("Plugins", "Plugins"), + ("Uninstall", "Afinstallér"), + ("Update", "Opdatér"), + ("Enable", "Aktivér"), + ("Disable", "Deaktivér"), + ("Options", "Valgmuligheder"), + ("resolution_original_tip", "Original skærmopløsning"), + ("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"), + ("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"), + ("Collapse toolbar", "Skjul værktøjsbjælke"), + ("Accept and Elevate", "Acceptér og elevér"), + ("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"), + ("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"), + ("Incoming connection", "Indgående forbindelse"), + ("Outgoing connection", "Udgående forbindelse"), + ("Exit", "Afslut"), + ("Open", "Åben"), + ("logout_tip", "Er du sikker på at du vil logge af?"), + ("Service", "Tjeneste"), + ("Start", "Start"), + ("Stop", "Stop"), + ("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."), + ("Sync with recent sessions", "Synkronisér med tidligere sessioner"), + ("Sort tags", "Sortér nøgleord"), + ("Open connection in new tab", "Åbn forbindelse i en ny fane"), + ("Move tab to new window", "Flyt fane i et nyt vindue"), + ("Can not be empty", "Kan ikke være tom"), + ("Already exists", "Findes allerede"), + ("Change Password", "Skift adgangskode"), + ("Refresh Password", "Genopfrisk adgangskode"), + ("ID", "ID"), + ("Grid View", "Gittervisning"), + ("List View", "Listevisning"), + ("Select", "Vælg"), + ("Toggle Tags", "Slå nøgleord til/fra"), + ("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"), + ("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"), + ("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."), + ("Change Color", "Skift farve"), + ("Primary Color", "Primær farve"), + ("HSV Color", "HSV farve"), + ("Installation Successful!", "Installation fuldført!"), + ("Installation failed!", "Installation mislykkedes!"), + ("Reverse mouse wheel", "Invertér musehjul"), + ("{} sessions", "{} sessioner"), + ("scam_title", "ADVARSEL: Du kan blive SVINDLET!"), + ("scam_text1", "Hvis du taler telefon med en person du IKKE kender, og IKKE stoler på, som har bedt dig om at bruge RustDesk til at forbinde til din PC, stop med det samme, og læg på omgående."), + ("scam_text2", "Det er højest sandsynligvis en svinder som forsøger at stjæle dine penge eller andre personlige oplysninger."), + ("Don't show again", "Vis ikke igen"), + ("I Agree", "Jeg accepterer"), + ("Decline", "Afvis"), + ("Timeout in minutes", "Udløbstid i minutter"), + ("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"), + ("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"), + ("Check for software update on startup", "Søg efter opdateringer ved opstart"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"), + ("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"), + ("Filter by intersection", "Filtrér efter intersection"), + ("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."), + ("No displays", "Ingen skærme"), + ("Open in new window", "Åbn i et nyt vindue"), + ("Show displays as individual windows", "Vis skærme som selvstændige vinduer"), + ("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"), + ("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."), + ("Change view", "Skift visning"), + ("Big tiles", "Store fliser"), + ("Small tiles", "Små fliser"), + ("List", "Liste"), + ("Virtual display", "Virtuel skærm"), + ("Plug out all", "Frakobl alt"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Aktivér blokering af brugerstyring"), + ("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (:).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (@?key=), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"@offentlig server\", nøglen er ikke nødvendig for offentlige servere.\n\nHvis du gerne vil tvinge brugen af en relay-forbindelse på den første forbindelse, tilføj \"/r\" efter ID'et, fx, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Tilstand 1"), + ("privacy_mode_impl_virtual_display_tip", "Tilstand 2"), + ("Enter privacy mode", "Start privatlivstilstand"), + ("Exit privacy mode", "Afslut privatlivstilstand"), + ("idd_not_support_under_win10_2004_tip", "Indirekte grafik drivere er ikke understøttet. Windows 10 version 2004 eller nyere er påkrævet."), + ("input_source_1_tip", "Input kilde 1"), + ("input_source_2_tip", "Input kilde 2"), + ("Swap control-command key", "Byt rundt på Control & Command tasterne"), + ("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"), + ("2FA code", "To-faktor kode"), + ("More", "Mere"), + ("enable-2fa-title", "Tænd for to-faktor godkendelse"), + ("enable-2fa-desc", "Åbn din godkendelsesapp nu. Du kan bruge en godkendelsesapp så som Authy, Microsoft eller Google Authenticator på din telefon eller din PC.\n\nScan QR koden med din app og indtast koden som din app fremviser, for at aktivere for to-faktor godkendelse."), + ("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"), + ("enter-2fa-title", "To-faktor godkendelse"), + ("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"), + ("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"), + ("Multiple Windows sessions found", "Flere Windows sessioner fundet"), + ("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"), + ("powered_by_me", "Drives af RustDesk"), + ("outgoing_only_desk_tip", "Dette er en brugertilpasset udgave.\nDu kan forbinde til andre enheder, men andre enheder kan ikke forbinde til din enhed."), + ("preset_password_warning", "Denne brugertilpassede udgave har en forudbestemt adgangskode. Alle der kender til denne adgangskode, kan få fuld adgang til din enhed. Hvis du ikke forventede dette, bør du afinstallere denne udgave af RustDesk med det samme."), + ("Security Alert", "Sikkerhedsalarm"), + ("My address book", "Min adressebog"), + ("Personal", "Personlig"), + ("Owner", "Ejer"), + ("Set shared password", "Sæt delt adgangskode"), + ("Exist in", "Findes i"), + ("Read-only", "Skrivebeskyttet"), + ("Read/Write", "Læse/Skrive"), + ("Full Control", "Fuld kontrol"), + ("share_warning_tip", "Felterne for oven er delt og synlige for andre."), + ("Everyone", "Alle"), + ("ab_web_console_tip", "Mere på web konsollen"), + ("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."), + ("Follow remote cursor", "Følg musemarkør på fjernforbindelse"), + ("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"), + ("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"), + ("no_audio_input_device_tip", "Ingen lydinput enhed fundet"), + ("Incoming", "Indgående"), + ("Outgoing", "Udgående"), + ("Clear Wayland screen selection", "Ryd Wayland skærmvalg"), + ("clear_Wayland_screen_selection_tip", "Efter at fravælge den valgte skærm, kan du genvælge skærmen som skal deles."), + ("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"), + ("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."), + ("texture_render_tip", "Brug tekstur-rendering for at gøre billedkvaliteten blødere. Du kan også prøve at deaktivere denne funktion, hvis du oplever problemer."), + ("Use texture rendering", "Anvend tekstur-rendering"), + ("Floating window", "Svævende vindue"), + ("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"), + ("Keep screen on", "Hold skærmen tændt"), + ("Never", "Aldrig"), + ("During controlled", "Imens under kontrol"), + ("During service is on", "Imens tjenesten kører"), + ("Capture screen using DirectX", "Optag skærm med DirectX"), + ("Back", "Tilbage"), + ("Apps", "Apps"), + ("Volume up", "Skru op for lyd"), + ("Volume down", "Skru ned for lyd"), + ("Power", "Tænd/Sluk"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Hvis du aktiverer denne funktion, kan du modtage to-faktor godkendelseskoden fra din robot. Den kan også fungere som en notifikation for forbindelsesanmodninger."), + ("enable-bot-desc", "1. Åbn en chat med @BotFather.\n2. Send kommandoen \"/newbot\". Du vil modtage en nøgle efter at have gennemført dette trin.\n3. Start en chat med din nyoprettede bot. Send en besked som begynder med skråstreg \"/\", som fx \"/hello\", for at aktivere den.\n"), + ("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"), + ("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"), + ("About RustDesk", "Om RustDesk"), + ("Send clipboard keystrokes", "Send udklipsholder tastetryk"), + ("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."), + ("Unlock with PIN", "Lås op med PIN"), + ("Requires at least {} characters", "Kræver mindst {} tegn"), + ("Wrong PIN", "Forkert PIN"), + ("Set PIN", "Sæt PIN"), + ("Enable trusted devices", "Aktivér troværdige enheder"), + ("Manage trusted devices", "Administrér troværdige enheder"), + ("Platform", "Platform"), + ("Days remaining", "Dage tilbage"), + ("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"), + ("Parent directory", "mappe"), + ("Resume", "Fortsæt"), + ("Invalid file name", "Ugyldigt filnavn"), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c604167ef9ff..dbc6efc2d391 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "TCP-Tunnelung"), ("Remove", "Entfernen"), ("Refresh random password", "Zufälliges Passwort erzeugen"), - ("Set your own password", "Eigenes Passwort setzen"), + ("Set your own password", "Eigenes Passwort festlegen"), ("Enable keyboard/mouse", "Tastatur und Maus aktivieren"), ("Enable clipboard", "Zwischenablage aktivieren"), ("Enable file transfer", "Dateiübertragung aktivieren"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), ("Lock after session end", "Nach Sitzungsende sperren"), - ("Insert", "Einfügen"), + ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), - ("Set permanent password", "Permanentes Passwort setzen"), + ("Set permanent password", "Permanentes Passwort festlegen"), ("Enable remote restart", "Entfernten Neustart aktivieren"), ("Restart remote device", "Entferntes Gerät neu starten"), ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), + ("Automatically record outgoing sessions", "Ausgehende Sitzungen automatisch aufzeichnen"), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -548,7 +549,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("Filter by intersection", "Nach Schnittmenge filtern"), ("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"), - ("Test", "Test"), + ("Test", "Testen"), ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."), ("No displays", "Keine Bildschirme"), ("Open in new window", "In einem neuen Fenster öffnen"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Alle ausschalten"), ("True color (4:4:4)", "True Color (4:4:4)"), ("Enable blocking user input", "Blockieren von Benutzereingaben aktivieren"), - ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen möchten, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), + ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "Modus 1"), ("privacy_mode_impl_virtual_display_tip", "Modus 2"), ("Enter privacy mode", "Datenschutzmodus aktivieren"), @@ -590,7 +591,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("My address book", "Mein Adressbuch"), ("Personal", "Persönlich"), ("Owner", "Eigentümer"), - ("Set shared password", "Geteiltes Passwort setzen"), + ("Set shared password", "Geteiltes Passwort festlegen"), ("Exist in", "Existiert in …?"), ("Read-only", "Nur lesen"), ("Read/Write", "Lesen/Schreiben"), @@ -630,6 +631,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Sind Sie sicher, dass Sie 2FA abbrechen möchten?"), ("cancel-bot-confirm-tip", "Sind Sie sicher, dass Sie Telegram-Bot abbrechen möchten?"), ("About RustDesk", "Über RustDesk"), - ("Send clipboard keystrokes", ""), + ("Send clipboard keystrokes", "Tastenanschläge aus der Zwischenablage senden"), + ("network_error_tip", "Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es dann erneut."), + ("Unlock with PIN", "Mit PIN entsperren"), + ("Requires at least {} characters", "Erfordert mindestens {} Zeichen"), + ("Wrong PIN", "Falsche PIN"), + ("Set PIN", "PIN festlegen"), + ("Enable trusted devices", "Vertrauenswürdige Geräte aktivieren"), + ("Manage trusted devices", "Vertrauenswürdige Geräte verwalten"), + ("Platform", "Plattform"), + ("Days remaining", "Verbleibende Tage"), + ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), + ("Parent directory", "Übergeordnetes Verzeichnis"), + ("Resume", "Fortsetzen"), + ("Invalid file name", "Ungültiger Dateiname"), + ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), + ("Authentication Required", "Authentifizierung erforderlich"), + ("Authenticate", "Authentifizieren"), + ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), + ("Download", "Herunterladen"), + ("Upload folder", "Ordner hochladen"), + ("Upload files", "Dateien hochladen"), + ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), + ("Update client clipboard", "Client-Zwischenablage aktualisieren"), + ("Untagged", "Unmarkiert"), + ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 731e2a5f3ce0..2979c1aaad56 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισαγωγή"), + ("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"), ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), @@ -146,9 +146,9 @@ 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", "Αναβάθμιση τώρα"), + ("Click to download", "Λήψη τώρα"), + ("Click to update", "Ενημέρωση τώρα"), ("Configure", "Διαμόρφωση"), ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), @@ -364,6 +364,7 @@ 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", "Διακοπή εγγραφής συνεδρίας"), @@ -510,7 +511,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", "Άνοιγμα σύνδεσης σε νέα καρτέλα"), @@ -591,19 +592,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", ""), @@ -618,11 +619,11 @@ 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 σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."), @@ -630,6 +631,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"), ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το Telegram bot;"), ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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", ""), + ("Download", ""), + ("Upload folder", "Μεταφόρτωση φακέλου"), + ("Upload files", "Μεταφόρτωση αρχείων"), + ("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"), + ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), + ("Untagged", "Χωρίς ετικέτα"), + ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index c4045fd8a58a..2295fd05cef7 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -232,5 +232,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Are you sure you want to cancel 2FA?"), ("cancel-bot-confirm-tip", "Are you sure you want to cancel Telegram bot?"), ("About RustDesk", ""), + ("network_error_tip", "Please check your network connection, then click retry."), + ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), + ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 190c693468d1..7370c2429fba 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -6,11 +6,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("desk_tip", "Via aparato povas esti alirita kun tiu identigilo kaj pasvorto"), ("Password", "Pasvorto"), ("Ready", "Preta"), - ("Established", ""), + ("Established", "Establis"), ("connecting_status", "Konektante al la reto RustDesk..."), ("Enable service", "Ebligi servon"), ("Start service", "Starti servon"), - ("Service is running", ""), + ("Service is running", "La servo funkcias"), ("Service is not running", "La servo ne funkcias"), ("not_ready_status", "Ne preta, bonvolu kontroli la retkonekto"), ("Control Remote Desktop", "Kontroli foran aparaton"), @@ -29,33 +29,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP tunneling", "Ebligi tunelado TCP"), ("IP Whitelisting", "Listo de IP akceptataj"), ("ID/Relay Server", "Identigila/Relajsa servilo"), - ("Import server config", "Enporti servilan agordon"), - ("Export Server Config", ""), + ("Import server config", "Importi servilan agordon"), + ("Export Server Config", "Eksporti servilan agordon"), ("Import server configuration successfully", "Importi servilan agordon sukcese"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Eksporti servilan agordon sukcese"), ("Invalid server configuration", "Nevalida servila agordo"), ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Via nova identigilo"), + ("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."), ("Website", "Retejo"), ("About", "Pri"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Farita kun koro en ĉi tiu ĥaosa mondo!"), + ("Privacy Statement", "Deklaro Pri Privateco"), ("Mute", "Muta"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), - ("Audio Input", "Aŭdia enigo"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive bitrate", ""), + ("Build Date", "konstruada dato"), + ("Version", "Versio"), + ("Home", "Hejmo"), + ("Audio Input", "Aŭdia Enigo"), + ("Enhancements", "Plibonigoj"), + ("Hardware Codec", "Aparataro Kodeko"), + ("Adaptive bitrate", "Adapta bitrapido"), ("ID Server", "Servilo de identigiloj"), - ("Relay Server", "Relajsa servilo"), + ("Relay Server", "Relajsa Servilo"), ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), @@ -83,35 +83,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Successful", "Sukceso"), ("Connected, waiting for image...", "Konektita, atendante bildon..."), ("Name", "Nomo"), - ("Type", ""), + ("Type", "Tipo"), ("Modified", "Modifita"), ("Size", "Grandeco"), ("Show Hidden Files", "Montri kaŝitajn dosierojn"), ("Receive", "Akcepti"), ("Send", "Sendi"), - ("Refresh File", ""), - ("Local", ""), - ("Remote", ""), + ("Refresh File", "Aktualigu Dosieron"), + ("Local", "Loka"), + ("Remote", "Fora"), ("Remote Computer", "Fora komputilo"), ("Local Computer", "Loka komputilo"), ("Confirm Delete", "Konfermi la forigo"), - ("Delete", ""), - ("Properties", ""), - ("Multi Select", ""), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", ""), - ("Not an empty directory", ""), - ("Are you sure you want to delete this file?", "Ĉu vi vere volas forigi tiun dosieron?"), - ("Are you sure you want to delete this empty directory?", ""), - ("Are you sure you want to delete the file of this directory?", ""), + ("Delete", "Forigi"), + ("Properties", "Propraĵoj"), + ("Multi Select", "Pluropa Elekto"), + ("Select All", "Elektu Ĉiujn"), + ("Unselect All", "Malelektu Ĉiujn"), + ("Empty Directory", "Malplena Dosierujo"), + ("Not an empty directory", "Ne Malplena Dosierujo"), + ("Are you sure you want to delete this file?", "Ĉu vi certas, ke vi volas forigi ĉi tiun dosieron?"), + ("Are you sure you want to delete this empty directory?", "Ĉu vi certas, ke vi volas forigi ĉi tiun malplenan dosierujon?"), + ("Are you sure you want to delete the file of this directory?", "Ĉu vi certa. ke vi volas forigi la dosieron de ĉi tiu dosierujo"), ("Do this for all conflicts", "Same por ĉiuj konfliktoj"), - ("This is irreversible!", ""), + ("This is irreversible!", "Ĉi tio estas neinversigebla!"), ("Deleting", "Forigado"), ("files", "dosiero"), ("Waiting", "Atendante..."), ("Finished", "Finita"), - ("Speed", ""), + ("Speed", "Rapideco"), ("Custom Image Quality", "Agordi bildan kvaliton"), ("Privacy mode", "Modo privata"), ("Block user input", "Bloki uzanta enigo"), @@ -127,10 +127,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Optimigi reakcia tempo"), ("Custom", ""), ("Show remote cursor", "Montri foran kursoron"), - ("Show quality monitor", ""), + ("Show quality monitor", "Montri kvalito monitoron"), ("Disable clipboard", "Malebligi poŝon"), ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), - ("Insert", "Enmeti"), + ("Insert Ctrl + Alt + Del", "Enmeti Ctrl + Alt + Del"), ("Insert Lock", "Ŝlosi foran komputilon"), ("Refresh", "Refreŝigi ekranon"), ("ID does not exist", "La identigilo ne ekzistas"), @@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Ago"), ("Add", "Aldoni"), ("Local Port", "Loka pordo"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Loka Adreso"), + ("Change Local Port", "Ŝanĝi Loka Pordo"), ("setup_server_tip", "Se vi bezonas pli rapida konekcio, vi povas krei vian propran servilon"), ("Too short, at least 6 characters.", "Tro mallonga, almenaŭ 6 signoj."), ("The confirmation is not identical.", "Ambaŭ enigoj ne kongruas"), @@ -203,23 +203,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Restarto deviga"), ("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"), ("x11 expected", "Bonvolu uzi x11"), - ("Port", ""), + ("Port", "Pordo"), ("Settings", "Agordoj"), ("Username", " Uzanta nomo"), ("Invalid port", "Pordo nevalida"), ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Connect via relay", ""), + ("Connect via relay", "Konekti per relajso"), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), - ("Login", "Konekti"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), - ("Logout", "Malkonekti"), + ("Login", "Ensaluti"), + ("Verify", "Kontrolis"), + ("Remember me", "Memori min"), + ("Trust this device", "Fidu ĉi tiun aparaton"), + ("Verification code", "Konfirmkodo"), + ("verification_tip", "Konfirmkodo estis sendita al la registrita retpoŝta adreso, enigu la konfirmkodon por daŭrigi ensaluti."), + ("Logout", "Elsaluti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), @@ -241,86 +241,86 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 prokura servilo"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"), ("Discovered", "Malkovritaj"), - ("install_daemon_tip", ""), + ("install_daemon_tip", "Por komenci ĉe ekŝargo, oni devas instali sisteman servon."), ("Remote ID", "Fora identigilo"), ("Paste", "Alglui"), - ("Paste here?", ""), + ("Paste here?", "Alglui ĉi tie?"), ("Are you sure to close the connection?", "Ĉu vi vere volas fermi la konekton?"), ("Download new version", "Elŝuti la novan version"), ("Touch mode", "Tuŝa modo"), - ("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", ""), + ("Mouse mode", "musa modo"), + ("One-Finger Tap", "Unufingra Frapeto"), + ("Left Mouse", "Maldekstra Muso"), + ("One-Long Tap", "Unulonga Frapeto"), + ("Two-Finger Tap", "Dufingra Frapeto"), + ("Right Mouse", "Deskra Muso"), + ("One-Finger Move", "Unufingra Movo"), + ("Double Tap & Move", "Duobla Frapeto & Movo"), + ("Mouse Drag", "Muso Trenadi"), + ("Three-Finger vertically", "Tri Figroj Vertikale"), + ("Mouse Wheel", "Musa Rado"), + ("Two-Finger Move", "Dufingra Movo"), + ("Canvas Move", "Kanvasa Movo"), + ("Pinch to Zoom", "Pinĉi al Zomo"), + ("Canvas Zoom", "Kanvasa Zomo"), ("Reset canvas", "Restarigi kanvaso"), ("No permission of file transfer", "Neniu permeso de dosiertransigo"), ("Note", "Notu"), - ("Connection", ""), - ("Share Screen", ""), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), - ("Screen Capture", ""), - ("Input Control", ""), - ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), - ("Do you accept?", ""), - ("Open System Setting", ""), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", ""), - ("android_input_permission_tip2", ""), - ("android_new_connection_tip", ""), - ("android_service_will_start_tip", ""), - ("android_stop_service_tip", ""), - ("android_version_audio_tip", ""), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("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", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), + ("Connection", "Konekto"), + ("Share Screen", "Kunhavigi Ekranon"), + ("Chat", "Babilo"), + ("Total", "Sumo"), + ("items", "eroj"), + ("Selected", "Elektita"), + ("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"), + ("android_input_permission_tip1", "Por ke fora aparato regu vian Android-aparaton per muso aŭ tuŝo, vi devas permesi al RustDesk uzi la servon \"Alirebleco\"."), + ("android_input_permission_tip2", "Bonvolu iri al la sekva paĝo de sistemaj agordoj, trovi kaj eniri [Instatajn Servojn], ŝalti la servon [RustDesk Enigo]."), + ("android_new_connection_tip", "Nova kontrolpeto estis ricevita, kiu volas kontroli vian nunan aparaton."), + ("android_service_will_start_tip", "Ŝalti \"Ekrankapto\" aŭtomate startos la servon, permesante al aliaj aparatoj peti konekton al via aparato."), + ("android_stop_service_tip", "Fermante la servon aŭtomate fermos ĉiujn establitajn konektojn."), + ("android_version_audio_tip", "La nuna versio da Android ne subtenas sonkapton, bonvolu ĝisdatigi al Android 10 aŭ pli alta."), + ("android_start_service_tip", "Frapu [Komenci servo] aŭ ebligu la permeson de [Ekrankapto] por komenci la servon de kundivido de ekrano."), + ("android_permission_may_not_change_tip", "Permesoj por establitaj konektoj neble estas ŝanĝitaj tuj ĝis rekonektitaj."), + ("Account", "Konto"), + ("Overwrite", "anstataŭigi"), + ("This file exists, skip or overwrite this file?", "Ĉi tiu dosiero ekzistas, ĉu preterlasi aŭ anstataŭi ĉi tiun dosieron?"), + ("Quit", "Forlasi"), + ("Help", "Helpi"), + ("Failed", "Malsukcesa"), + ("Succeeded", "Sukcesa"), + ("Someone turns on privacy mode, exit", "Iu ŝaltas modon privata, Eliro"), + ("Unsupported", "Nesubtenata"), + ("Peer denied", "Samulo rifuzita"), + ("Please install plugins", "Bonvolu instali kromprogramojn"), + ("Peer exit", "Samulo eliras"), + ("Failed to turn off", "Malsukcesis malŝalti"), + ("Turned off", "Malŝaltita"), + ("Language", "Lingvo"), + ("Keep RustDesk background service", "Tenu RustDesk fonan servon"), + ("Ignore Battery Optimizations", "Ignoru Bateria Optimumigojn"), + ("android_open_battery_optimizations_tip", "Se vi volas malŝalti ĉi tiun funkcion, bonvolu iri al la sekva paĝo de agordoj de la aplikaĵo de RustDesk, trovi kaj eniri [Baterio], Malmarku [Senrestrikta]"), + ("Start on boot", "Komencu ĉe ekfunkciigo"), + ("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"), + ("Connection not allowed", "Konekto ne rajtas"), ("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", ""), + ("Map mode", "Mapa modo"), + ("Translate mode", "Traduki modo"), + ("Use permanent password", "Uzu permanenta pasvorto"), + ("Use both passwords", "Uzu ambaŭ pasvorto"), + ("Set permanent password", "Starigi permanenta pasvorto"), + ("Enable remote restart", "Permesi fora restartas"), + ("Restart remote device", "Restartu fora aparato"), + ("Are you sure you want to restart", "Ĉu vi certas, ke vi volas restarti"), + ("Restarting remote device", "Restartas fora aparato"), + ("remote_restarting_tip", "Fora aparato restartiĝas, bonvolu fermi ĉi tiun mesaĝkeston kaj rekonekti kun permanenta pasvorto post iom da tempo"), + ("Copied", "Kopiita"), ("Exit Fullscreen", "Eliru Plenekranon"), ("Fullscreen", "Plenekrane"), ("Mobile Actions", "Poŝtelefonaj Agoj"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporcio"), ("Image Quality", "Bilda Kvalito"), ("Scroll Style", "Ruluma Stilo"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Montri Ilobreton"), + ("Hide Toolbar", "Kaŝi Ilobreton"), ("Direct Connection", "Rekta Konekto"), ("Relay Connection", "Relajsa Konekto"), ("Secure Connection", "Sekura Konekto"), @@ -364,6 +364,7 @@ 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", ""), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0e94071e4a15..3ad77afe590a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -48,7 +48,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), ("Build Date", "Fecha de compilación"), - ("Version", "¨Versión"), + ("Version", "Versión"), ("Home", "Inicio"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Mostrar calidad del monitor"), ("Disable clipboard", "Deshabilitar portapapeles"), ("Lock after session end", "Bloquear después del final de la sesión"), - ("Insert", "Insertar"), + ("Insert Ctrl + Alt + Del", "Insertar Ctrl + Alt + Del"), ("Insert Lock", "Insertar bloqueo"), ("Refresh", "Actualizar"), ("ID does not exist", "La ID no existe"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), @@ -394,7 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Aceptar sesiones a través de clic"), ("Accept sessions via both", "Aceptar sesiones a través de ambos"), ("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"), - ("One-time Password", "Constaseña de un solo uso"), + ("One-time Password", "Contraseña de un solo uso"), ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), ("Request access to your device", "Solicitud de acceso a su dispositivo"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Abre un chat con @BotFather.\n2, Envía el comando \"/newbot\". Recibirás un token tras completar esta paso.\n3, Inicia un chat con tu bot recién creado. Envía un mensaje que comience con una barra (\"/\") como \"/hola\" para activarlo.\n"), ("cancel-2fa-confirm-tip", "¿Seguro que quieres cancelar 2FA?"), ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "Acerca de RustDesk"), + ("Send clipboard keystrokes", "Enviar pulsaciones de teclas"), + ("network_error_tip", "Por fvor, comprueba tu conexión de red e inténtalo de nuevo."), + ("Unlock with PIN", "Desbloquear con PIN"), + ("Requires at least {} characters", "Se necesitan al menos {} caracteres"), + ("Wrong PIN", "PIN erróneo"), + ("Set PIN", "Establecer PIN"), + ("Enable trusted devices", "Habilitar dispositivos de confianza"), + ("Manage trusted devices", "Gestionar dispositivos de confianza"), + ("Platform", "Plataforma"), + ("Days remaining", "Días restantes"), + ("enable-trusted-devices-tip", "Omitir la verificación en dos fases en dispositivos de confianza"), + ("Parent directory", "Directorio superior"), + ("Resume", "Continuar"), + ("Invalid file name", "Nombre de archivo no válido"), + ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), + ("Authentication Required", "Se requiere autenticación"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Puedes introducir una ID en el mismo servidor, el cliente web no soporta acceso vía IP.\nSi quieres acceder a un dispositivo en otro servidor, por favor, agrega la dirección del servidor (@?clave=), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres accedder a un dispositivo en un servidor público, por favor, introduce \"@public\", la clave no es necesaria para el servidor público."), + ("Download", "Descarga"), + ("Upload folder", "Subir carpeta"), + ("Upload files", "Subir archivos"), + ("Clipboard is synchronized", "Portapapeles sincronizado"), + ("Update client clipboard", "Actualizar portapapeles del cliente"), + ("Untagged", "Sin itiquetar"), + ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 81fc2e77a213..b38b55bd615d 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "Sisesta lukk"), ("Refresh", ""), ("ID does not exist", ""), @@ -364,6 +364,7 @@ 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", ""), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 48efcf789bc0..efb281496df0 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Erakutsi kalitate monitorea"), ("Disable clipboard", "Desgaitu arbela"), ("Lock after session end", "Blokeatu sesioa amaitu ostean"), - ("Insert", "Sartu"), + ("Insert Ctrl + Alt + Del", "Sartu Ctrl + Alt + Del"), ("Insert Lock", "Sarrera-blokeoa"), ("Refresh", "Freskatu"), ("ID does not exist", "IDa ez da existitzen"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabatzen"), ("Directory", "Direktorioa"), ("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"), + ("Automatically record outgoing sessions", ""), ("Change", "Aldatu"), ("Start session recording", "Hasi saioaren grabaketa"), ("Stop session recording", "Gelditu saioaren grabaketa"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e6754205a5bd..d1d3d47679a6 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "نمایش کیفیت مانیتور"), ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), - ("Insert", "افزودن"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del افزودن"), ("Insert Lock", "قفل کردن سیستم"), ("Refresh", "تازه سازی"), ("ID does not exist", "شناسه وجود ندارد"), @@ -364,6 +364,7 @@ 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", "توقف ضبط جلسه"), @@ -610,26 +611,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Keep screen on", ""), - ("Never", ""), + ("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", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Capture screen using DirectX", "DirectX تصویربرداری از صفحه نمایش با استفاده از"), + ("Back", "برگشت"), + ("Apps", "برنامه ها"), + ("Volume up", "افزایش صدا"), + ("Volume down", "کاهش صدا"), ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), + ("Telegram bot", "ربات تلگرام"), + ("enable-bot-tip", "اگر این ویژگی را فعال کنید، می توانید کد تائید دو مرحله ای را از ربات خود دریافت کنید. همچنین می تواند به عنوان یک اعلان اتصال عمل کند."), ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index e0b0306dd74b..19b4e58a63b9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"), - ("Insert", "Envoyer"), + ("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"), @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), ("Set Password", "Définir le mot de passe"), ("OS Password", "Mot de passe du système d'exploitation"), - ("install_tip", "Vous utilisez une version non installée. En raison des restrictions UAC, en tant que terminal contrôlé, dans certains cas, il ne sera pas en mesure de contrôler la souris et le clavier ou d'enregistrer l'écran. Veuillez cliquer sur le bouton ci-dessous pour installer RustDesk au système pour éviter la question ci-dessus."), + ("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"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Enregistrement"), ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifier"), ("Start session recording", "Commencer l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 401b52c28830..a5099f487612 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -1,16 +1,16 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", ""), + ("Status", "מצב"), ("Your Desktop", ""), ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), - ("Password", ""), - ("Ready", ""), + ("Password", "סיסמא"), + ("Ready", "מוכן"), ("Established", ""), ("connecting_status", "מתחבר לרשת RustDesk..."), ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), + ("Start service", "התחל שירות"), + ("Service is running", "השירות פעיל"), ("Service is not running", ""), ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), ("Control Remote Desktop", ""), @@ -20,7 +20,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Address book", ""), ("Confirmation", ""), ("TCP tunneling", ""), - ("Remove", ""), + ("Remove", "הסר"), ("Refresh random password", ""), ("Set your own password", ""), ("Enable keyboard/mouse", ""), @@ -35,21 +35,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export server configuration successfully", ""), ("Invalid server configuration", ""), ("Clipboard is empty", ""), - ("Stop service", ""), + ("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", ""), + ("Website", "דף הבית"), + ("About", "אודות"), ("Slogan_tip", "נוצר בלב בעולם הזה הכאוטי!"), ("Privacy Statement", ""), - ("Mute", ""), + ("Mute", "השתק"), ("Build Date", "תאריך בנייה"), ("Version", ""), - ("Home", ""), + ("Home", "בית"), ("Audio Input", "קלט שמע"), ("Enhancements", ""), ("Hardware Codec", "קודק חומרה"), @@ -63,18 +63,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "עדיין לא נתמך על ידי השרת"), ("Not available", ""), ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), + ("Cancel", "ביטול"), + ("Skip", "דלג"), + ("Close", "סגור"), + ("Retry", "נזה שוב"), + ("OK", "אישור"), ("Password Required", "נדרשת סיסמה"), ("Please enter your password", ""), ("Remember password", ""), ("Wrong Password", "סיסמה שגויה"), ("Do you want to enter again?", ""), ("Connection Error", "שגיאת חיבור"), - ("Error", ""), + ("Error", "שגיאה"), ("Reset by the peer", ""), ("Connecting...", ""), ("Connection in progress. Please wait.", ""), @@ -82,20 +82,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login Error", "שגיאת התחברות"), ("Successful", ""), ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), + ("Name", "שם"), + ("Type", "סוג"), ("Modified", ""), - ("Size", ""), + ("Size", "גודל"), ("Show Hidden Files", "הצג קבצים נסתרים"), ("Receive", ""), - ("Send", ""), + ("Send", "שלח"), ("Refresh File", "רענן קובץ"), - ("Local", ""), + ("Local", "מקומי"), ("Remote", ""), ("Remote Computer", "מחשב מרוחק"), ("Local Computer", "מחשב מקומי"), ("Confirm Delete", "אשר מחיקה"), - ("Delete", ""), + ("Delete", "מחק"), ("Properties", ""), ("Multi Select", "בחירה מרובה"), ("Select All", "בחר הכל"), @@ -108,10 +108,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do this for all conflicts", ""), ("This is irreversible!", ""), ("Deleting", ""), - ("files", ""), + ("files", "קבצים"), ("Waiting", ""), - ("Finished", ""), - ("Speed", ""), + ("Finished", "הסתיים"), + ("Speed", "מהירות"), ("Custom Image Quality", "איכות תמונה מותאמת אישית"), ("Privacy mode", ""), ("Block user input", ""), @@ -125,14 +125,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", ""), ("Balanced", ""), ("Optimize reaction time", ""), - ("Custom", ""), + ("Custom", "מותאם אישית"), ("Show remote cursor", ""), ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "הוסף נעילה"), - ("Refresh", ""), + ("Refresh", "רענן"), ("ID does not exist", ""), ("Failed to connect to rendezvous server", ""), ("Please try later", ""), @@ -153,7 +153,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), ("Installing ...", ""), - ("Install", ""), + ("Install", "התקן"), ("Installation", ""), ("Installation Path", "נתיב התקנה"), ("Create start menu shortcuts", ""), @@ -161,14 +161,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), ("Accept and Install", "קבל והתקן"), ("End-user license agreement", ""), - ("Generating ...", ""), + ("Generating ...", "יוצר ..."), ("Your installation is lower version.", ""), ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש במנהרה"), ("Listening ...", ""), ("Remote Host", "מארח מרוחק"), ("Remote Port", "פורט מרוחק"), ("Action", ""), - ("Add", ""), + ("Add", "הוסף"), ("Local Port", "פורט מקומי"), ("Local Address", "כתובת מקומית"), ("Change Local Port", "שנה פורט מקומי"), @@ -191,7 +191,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP session sharing", ""), ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), ("Enable direct IP access", ""), - ("Rename", ""), + ("Rename", "שנה שם"), ("Space", ""), ("Create desktop shortcut", ""), ("Change Path", "שנה נתיב"), @@ -203,9 +203,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", ""), ("Unsupported display server", ""), ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), + ("Port", "יציאה"), + ("Settings", "הגדרות"), + ("Username", "שם משתמש"), ("Invalid port", ""), ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), @@ -220,10 +220,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Verification code", ""), ("verification_tip", "קוד אימות נשלח לכתובת הדוא\"ל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), + ("Tags", "תגים"), + ("Search ID", "חפש מזהה"), ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), - ("Add ID", ""), + ("Add ID", "הוסף מזהה"), ("Add Tag", "הוסף תג"), ("Unselect all tags", ""), ("Network error", ""), @@ -236,14 +236,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Favorites", ""), ("Add to Favorites", "הוסף למועדפים"), ("Remove from Favorites", "הסר מהמועדפים"), - ("Empty", ""), + ("Empty", "ריק"), ("Invalid folder name", ""), ("Socks5 Proxy", "פרוקסי Socks5"), ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), ("Discovered", ""), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), ("Remote ID", ""), - ("Paste", ""), + ("Paste", "הדבק"), ("Paste here?", ""), ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), ("Download new version", ""), @@ -265,11 +265,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Zoom", "זום בד"), ("Reset canvas", ""), ("No permission of file transfer", ""), - ("Note", ""), + ("Note", "הערה"), ("Connection", ""), ("Share Screen", "שיתוף מסך"), - ("Chat", ""), - ("Total", ""), + ("Chat", "צ'אט"), + ("Total", "הכל"), ("items", ""), ("Selected", ""), ("Screen Capture", "לכידת מסך"), @@ -291,10 +291,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Account", ""), ("Overwrite", ""), ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), + ("Quit", "צא"), + ("Help", "עזרה"), + ("Failed", "נכשל"), + ("Succeeded", "הצליח"), ("Someone turns on privacy mode, exit", ""), ("Unsupported", ""), ("Peer denied", ""), @@ -322,7 +322,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "המכשיר המרוחק מתחיל מחדש, אנא סגור את תיבת ההודעה הזו והתחבר מחדש עם סיסמה קבועה לאחר זמן מה"), ("Copied", ""), ("Exit Fullscreen", "יציאה ממסך מלא"), - ("Fullscreen", ""), + ("Fullscreen", "מסך מלא"), ("Mobile Actions", "פעולות ניידות"), ("Select Monitor", "בחר מסך"), ("Control Actions", "פעולות בקרה"), @@ -338,24 +338,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "חיבור לא מאובטח"), ("Scale original", ""), ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), + ("General", "כללי"), + ("Security", "אבטחה"), + ("Theme", "ערכת נושא"), ("Dark Theme", "ערכת נושא כהה"), ("Light Theme", "ערכת נושא בהירה"), - ("Dark", ""), - ("Light", ""), + ("Dark", "כהה"), + ("Light", "בהיר"), ("Follow System", "עקוב אחר המערכת"), ("Enable hardware codec", ""), ("Unlock Security Settings", "פתח הגדרות אבטחה"), ("Enable audio", ""), ("Unlock Network Settings", "פתח הגדרות רשת"), - ("Server", ""), + ("Server", "שרת"), ("Direct IP Access", "גישה ישירה ל-IP"), - ("Proxy", ""), - ("Apply", ""), + ("Proxy", "פרוקסי"), + ("Apply", "החל"), ("Disconnect all devices?", ""), - ("Clear", ""), + ("Clear", "נקה"), ("Audio Input Device", "מכשיר קלט שמע"), ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), ("Network", ""), @@ -364,7 +364,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), - ("Change", ""), + ("Automatically record outgoing sessions", ""), + ("Change", "שנה"), ("Start session recording", ""), ("Stop session recording", ""), ("Enable recording session", ""), @@ -375,7 +376,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), ("Disconnected", ""), - ("Other", ""), + ("Other", "אחר"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "הגדרות מקלדת"), ("Full Access", "גישה מלאה"), @@ -385,8 +386,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("JumpLink", "הצג"), ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), + ("This PC", "מחשב זה"), + ("or", "או"), ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), @@ -402,10 +403,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "אפשר הסתרה רק אם מקבלים סשנים דרך סיסמה ומשתמשים בסיסמה קבועה"), ("wayland_experiment_tip", "תמיכה ב-Wayland נמצאת בשלב ניסיוני, אנא השתמש ב-X11 אם אתה זקוק לגישה לא מלווה."), ("Right click to select tabs", ""), - ("Skipped", ""), + ("Skipped", "דולג"), ("Add to address book", ""), - ("Group", ""), - ("Search", ""), + ("Group", "קבוצה"), + ("Search", "חפש"), ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), @@ -414,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב אחרי קלט\"."), ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרשאות \"הקלטת שמע\"."), ("request_elevation_tip", "ניתן גם לבקש הגבהה אם יש מישהו בצד המרוחק."), - ("Wait", ""), + ("Wait", "המתן"), ("Elevation Error", "שגיאת הגבהה"), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -433,30 +434,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", "החלף צדדים"), ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Display", "תצוגה"), ("Default View Style", "סגנון תצוגה ברירת מחדל"), ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), ("Default Image Quality", "איכות תמונה ברירת מחדל"), ("Default Codec", "קודק ברירת מחדל"), ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), + ("FPS", "FPS"), + ("Auto", "אוטומטי"), ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), - ("Voice call", ""), + ("Voice call", "שיחה קולית"), ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", "ייתכן שלא ניתן להתחבר ישירות; ניתן לנסות להתחבר דרך ריליי. בנוסף, אם ברצונך להשתמש בריליי בניסיון הראשון שלך, תוכל להוסיף את הסיומת \"/r\" למזהה או לבחור באפשרות \"התחבר תמיד דרך ריליי\" בכרטיס של הסשנים האחרונים אם קיים."), ("Reconnect", ""), - ("Codec", ""), + ("Codec", "קודק"), ("Resolution", ""), ("No transfers in progress", ""), ("Set one-time password length", ""), ("RDP Settings", "הגדרות RDP"), - ("Sort by", ""), + ("Sort by", "מיין לפי"), ("New Connection", "חיבור חדש"), ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Minimize", "הקטן"), + ("Maximize", "הגדל"), ("Your Device", "המכשיר שלך"), ("empty_recent_tip", "אופס, אין סשנים אחרונים!\nהגיע הזמן לתכנן חדש."), ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבוא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), @@ -482,19 +483,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "אנא התקן שולחן עבודה GNOME"), ("No need to elevate", ""), ("System Sound", "צליל מערכת"), - ("Default", ""), - ("New RDP", ""), + ("Default", "ברירת מחדל"), + ("New RDP", "RDP חדש"), ("Fingerprint", ""), ("Copy Fingerprint", "העתק טביעת אצבע"), ("no fingerprints", "אין טביעות אצבע"), ("Select a peer", ""), ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), + ("Plugins", "תוספים"), + ("Uninstall", "הסר"), + ("Update", "עדכן"), + ("Enable", "פועל"), + ("Disable", "כבוי"), + ("Options", "אפשרויות"), ("resolution_original_tip", "רזולוציה מקורית"), ("resolution_fit_local_tip", "התאם לרזולוציה מקומית"), ("resolution_custom_tip", "רזולוציה מותאמת אישית"), @@ -504,12 +505,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), ("Incoming connection", ""), ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Exit", "צא"), + ("Open", "פתח"), ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Start", "התחל"), + ("Stop", "עצור"), ("exceed_max_devices", "הגעת למספר המקסימלי של מכשירים שניתן לנהל."), ("Sync with recent sessions", ""), ("Sort tags", ""), @@ -519,10 +520,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Already exists", ""), ("Change Password", "שנה סיסמה"), ("Refresh Password", "רענן סיסמה"), - ("ID", ""), + ("ID", "מזהה"), ("Grid View", "תצוגת רשת"), ("List View", "תצוגת רשימה"), - ("Select", ""), + ("Select", "בחר"), ("Toggle Tags", "החלף תגיות"), ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), ("push_ab_failed_tip", "נכשל בסנכרון ספר הכתובות לשרת"), @@ -538,8 +539,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), + ("I Agree", "אני מסכים"), + ("Decline", "דחה"), ("Timeout in minutes", ""), ("auto_disconnect_option_tip", "סגור באופן אוטומטי סשנים נכנסים במקרה של חוסר פעילות של המשתמש"), ("Connection failed due to inactivity", "התנתקות אוטומטית בגלל חוסר פעילות"), @@ -548,7 +549,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "נכשל ברענון קבוצה"), ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Test", "בדיקה"), ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), ("No displays", ""), ("Open in new window", ""), @@ -558,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change view", ""), ("Big tiles", ""), ("Small tiles", ""), - ("List", ""), + ("List", "רשימה"), ("Virtual display", ""), ("Plug out all", ""), ("True color (4:4:4)", ""), @@ -574,7 +575,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Swap control-command key", ""), ("swap-left-right-mouse", "החלף בין כפתור העכבר השמאלי לימני"), ("2FA code", "קוד אימות דו-שלבי"), - ("More", ""), + ("More", "עוד"), ("enable-2fa-title", "הפעל אימות דו-שלבי"), ("enable-2fa-desc", "אנא הגדר כעת את האפליקציה שלך לאימות. תוכל להשתמש באפליקציית אימות כגון Authy, Microsoft או Google Authenticator בטלפון או במחשב שלך.\n\nסרוק את קוד ה-QR עם האפליקציה שלך והזן את הקוד שהאפליקציה מציגה כדי להפעיל את אימות הדו-שלבי."), ("wrong-2fa-code", "לא ניתן לאמת את הקוד. בדוק שהקוד והגדרות הזמן המקומיות נכונות"), @@ -619,8 +620,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During controlled", ""), ("During service is on", ""), ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), + ("Back", "חזור"), + ("Apps", "אפליקציות"), ("Volume up", ""), ("Volume down", ""), ("Power", ""), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("About RustDesk", "אודות RustDesk"), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong 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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 778eeacfa67b..ba4723b8a2f3 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži kvalitetu monitora"), ("Disable clipboard", "Zabrani međuspremnik"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osvježi"), ("ID does not exist", "ID ne postoji"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Mapa"), ("Automatically record incoming sessions", "Automatski snimi dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promijeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21e898c21c6b..7b149938e1aa 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -1,94 +1,94 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Státusz"), + ("Status", "Állapot"), ("Your Desktop", "Saját asztal"), ("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 tunneling"), - ("Remove", "Eltávolít"), + ("TCP tunneling", "TCP-tunneling"), + ("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 tunneling engedélyezése"), + ("Enable TCP tunneling", "TCP-tunneling engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "ID/Relay 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"), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("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."), - ("Website", "Weboldal"), - ("About", "Rólunk"), - ("Slogan_tip", ""), + ("Website", "Webhely"), + ("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", "Hardware kodek"), + ("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 helytelen."), ("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"), + ("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örölni szeretné 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,46 @@ 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", ""), + ("Show quality monitor", "Minőségi monitor megjeleníté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", ""), + ("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"), + ("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álata során. 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..."), + ("config_acc", "A távoli vezérléshez a RustDesknek „Kisegítő lehetőségek” 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í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."), + ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc 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 licenc szerződés"), + ("Generating ...", "Előállítá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 ...", "Keresés..."), + ("not_close_tcp_tip", "Ne zárja be ezt az ablakot amíg TCP-tunnelinget használ"), + ("Listening ...", "Figyelés…"), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -172,7 +172,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,48 +180,48 @@ 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"), ("Rename", "Átnevezés"), - ("Space", ""), + ("Space", "Szóköz"), ("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", ""), - ("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", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Ellenőrzés"), + ("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ód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), ("Logout", "Kilépés"), - ("Tags", "Tagok"), - ("Search ID", "Azonosító keresése..."), + ("Tags", "Címkék"), + ("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 +230,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 +244,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,99 +259,99 @@ 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"), - ("Chat", "Chat"), + ("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", "A távoli vezérléshez engedélyezze a „Kisegítő lehetőségek” lehetőséget."), ("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őrögzítés” bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök kapcsolódási kérelmet küldhessenek"), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „továbbító-kiszolgáló-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."), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", "A csatlakozás nem engedélyezett"), - ("Legacy mode", ""), - ("Map mode", ""), + ("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 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"), ("Use permanent password", "Állandó jelszó használata"), ("Use both passwords", "Mindkét jelszó használata"), ("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ő"), - ("Mobile Actions", "mobil műveletek"), + ("Mobile Actions", "Mobil műveletek"), ("Select Monitor", "Válasszon képernyőt"), ("Control Actions", "Irányítási műveletek"), ("Display Settings", "Megjelenítési beállítások"), ("Ratio", "Arány"), ("Image Quality", "Képminőség"), ("Scroll Style", "Görgetési stílus"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), - ("Direct Connection", "Közvetlen kapcsolat"), - ("Relay Connection", "Közvetett csatlakozás"), - ("Secure Connection", "Biztonságos kapcsolat"), - ("Insecure Connection", "Nem biztonságos kapcsolat"), + ("Show Toolbar", "Eszköztár megjelenítése"), + ("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 kapcsolódás"), + ("Insecure Connection", "Nem biztonságos kapcsolódás"), ("Scale original", "Eredeti méretarány"), ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), ("Security", "Biztonság"), ("Theme", "Téma"), ("Dark Theme", "Sötét téma"), - ("Light Theme", ""), + ("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?"), @@ -359,277 +359,302 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Audio bemeneti eszköz"), ("Use IP Whitelisting", "Engedélyezési lista használata"), ("Network", "Hálózat"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Eszköztár kitűzése"), + ("Unpin Toolbar", "Eszköztár kitűzésének feloldása"), ("Recording", "Felvétel"), ("Directory", "Könyvtár"), ("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"), + ("Automatically record outgoing sessions", "A kimenő munkamenetek automatikus rögzítése"), ("Change", "Változtatás"), ("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", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", "Szétkapcsolva"), + ("Prompt", "Kérés"), + ("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", "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"), ("Continue with", "Folytatás a következővel"), - ("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...", ""), + ("Elevate", "Hozzáférés engedélyezése"), + ("Zoom cursor", "Kurzor nagyítása"), + ("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...", "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álj ideiglenes jelszót"), + ("Use one-time password", "Használjon ideiglenes jelszót"), ("One-time password length", "Egyszer használatos jelszó hossza"), - ("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", ""), + ("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ű. 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", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("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_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", "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", "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 az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("Request Elevation", "Emelt szintű jogok igénylése"), + ("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"), + ("digit", "szám"), + ("special character", "különleges karakter"), + ("length>=8", "hossz>=8"), + ("Weak", "Gyenge"), + ("Medium", "Közepes"), + ("Strong", "Erős"), + ("Switch Sides", "Oldalváltás"), + ("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"), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", "Kódek"), + ("Default Codec", "Alapértelmezett kodek"), + ("Bitrate", "Bitsebesség"), + ("FPS", "FPS"), + ("Auto", "Automatikus"), + ("Other Default Options", "Egyéb alapértelmezett beállítások"), + ("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 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", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New 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", ""), + ("No transfers in progress", "Nincs folyamatban átvitel"), + ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), + ("RDP Settings", "RDP beállítások"), + ("Sort by", "Rendezés"), + ("New Connection", "Új kapcsolat"), + ("Restore", "Visszaállítás"), + ("Minimize", "Minimalizálás"), + ("Maximize", "Maximalizálás"), + ("Your Device", "Az Ön eszköze"), + ("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."), + ("eg: admin", "pl: adminisztrátor"), + ("Empty Username", "Üres felhasználónév"), + ("Empty Password", "Üres jelszó"), + ("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"), + ("login_linux_tip", "Az X-asztal munkamenet megnyitásához be kell jelentkeznie egy távoli Linux-fiókba."), + ("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"), + ("remember_account_tip", "Emlékezzen erre a fiókra"), + ("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."), + ("OS Account", "OS fiók"), + ("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", "Telepítse az Xorgot."), + ("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."), + ("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"), + ("Copy Fingerprint", "Ujjlenyomat másolása"), + ("no fingerprints", "nincsenek ujjlenyomatok"), + ("Select a peer", "Egy távoli állomás kiválasztása"), + ("Select peers", "Távoli állomások kiválasztása"), + ("Plugins", "Beépülő modulok"), + ("Uninstall", "Eltávolítás"), + ("Update", "Frissítés"), + ("Enable", "Engedélyezés"), + ("Disable", "Letiltás"), + ("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", "Testre szabható felbontás"), + ("Collapse toolbar", "Eszköztár összecsukása"), + ("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 ki szeretne lépni?"), ("Service", "Szolgáltatás"), ("Start", "Indítás"), ("Stop", "Leállítás"), - ("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", ""), - ("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", ""), + ("exceed_max_devices", "Elérte a felügyelt eszközök maximális számát."), + ("Sync with recent sessions", "Szinkronizálás a legutóbbi munkamenetekkel"), + ("Sort tags", "Címkék rendezése"), + ("Open connection in new tab", "Kapcsolat megnyitása új lapon"), + ("Move tab to new window", "Lap áthelyezése új ablakba"), + ("Can not be empty", "Nem lehet üres"), + ("Already exists", "Már létezik"), + ("Change Password", "Jelszó módosítása"), + ("Refresh Password", "Jelszó frissítése"), + ("ID", "ID"), + ("Grid View", "Mozaik nézet"), + ("List View", "Lista nézet"), + ("Select", "Kiválasztás"), + ("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 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"), + ("HSV Color", "HSV szín"), + ("Installation Successful!", "Sikeres telepítés!"), + ("Installation failed!", "A telepítés nem sikerült!"), + ("Reverse mouse wheel", "Fordított egérgörgő"), + ("{} sessions", "{} munkamenet"), + ("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 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", "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"), + ("Test", "Teszt"), + ("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."), + ("No displays", "Nincsenek kijelzők"), + ("Open in new window", "Megnyitás új ablakban"), + ("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"), + ("Use all my displays for the remote session", "Az összes kijelzőm használata a távoli munkamenethez"), + ("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött webhely."), + ("Change view", "Nézet módosítása"), + ("Big tiles", "Nagy csempék"), + ("Small tiles", "Kis csempék"), + ("List", "Lista"), + ("Virtual display", "Virtuális kijelző"), + ("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 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"), + ("Exit privacy mode", "Lépjen ki az adatvédelmi módból"), + ("idd_not_support_under_win10_2004_tip", "A közvetett grafikus illesztőprogram nem támogatott. Windows 10, 2004-es vagy újabb verzió szükséges."), + ("input_source_1_tip", "1. bemeneti forrás"), + ("input_source_2_tip", "2. bemeneti forrás"), + ("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ábbiak"), + ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), + ("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-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", "Válassza ki a munkamenetet, amelyhez kapcsolódni szeretne"), + ("powered_by_me", "Üzemeltető: RustDesk"), + ("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", "Saját címjegyzék"), + ("Personal", "Személyes"), + ("Owner", "Tulajdonos"), + ("Set shared password", "Megosztott jelszó beállítása"), + ("Exist in", "Létezik"), + ("Read-only", "Csak olvasható"), + ("Read/Write", "Olvasás/Írás"), + ("Full Control", "Teljes ellenőrzés"), + ("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 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"), + ("default_proxy_tip", "A szabványos protokoll és port SOCKS5 és 1080"), + ("no_audio_input_device_tip", "Nem található hangbemeneti eszköz."), + ("Incoming", "Bejövő"), + ("Outgoing", "Kimenő"), + ("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"), + ("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."), + ("confirm_clear_Wayland_screen_selection_tip", "Biztos, hogy törölni szeretné a Wayland képernyő kiválasztását?"), + ("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."), + ("texture_render_tip", "Használja a textúra renderelést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha renderelési problémái vannak."), + ("Use texture rendering", "Textúra renderelés használata"), + ("Floating window", "Lebegő ablak"), + ("floating_window_tip", "Segít, ha a RustDesk a háttérben fut."), + ("Keep screen on", "Tartsa a képernyőt bekapcsolva"), + ("Never", "Soha"), + ("During controlled", "Amikor ellenőrzött"), + ("During service is on", "Amikor a szolgáltatás fut"), + ("Capture screen using DirectX", "Képernyő rögzítése DirectX használatával"), + ("Back", "Vissza"), + ("Apps", "Alkalmazások"), + ("Volume up", "Hangerő fel"), + ("Volume down", "Hangerő le"), + ("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", "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"), + ("Set PIN", "PIN-kód beállítása"), + ("Enable trusted devices", "Megbízható eszközök engedélyezése"), + ("Manage trusted devices", "Megbízható eszközök kezelése"), + ("Platform", "Platform"), + ("Days remaining", "Hátralévő napok"), + ("enable-trusted-devices-tip", "A 2FA-ellenőrzés kihagyása megbízható eszközökön"), + ("Parent directory", "Szülőkönyvtár"), + ("Resume", "Folytatás"), + ("Invalid file name", "Érvénytelen fájlnév"), + ("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 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", "A kliens vágólapjának frissítése"), + ("Untagged", "Címkézetlen"), + ("new-version-of-{}-tip", "A(z) {} új verziója"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index e13c088fde48..7d90a3ea4388 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Desktop Anda"), - ("desk_tip", "Desktop Anda dapat diakses dengan ID dan kata sandi ini."), + ("Your Desktop", "Layar Utama"), + ("desk_tip", "Layar kamu dapat diakses dengan ID dan kata sandi ini."), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), ("Established", "Didirikan"), @@ -12,17 +12,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start service", "Mulai Layanan"), ("Service is running", "Layanan berjalan"), ("Service is not running", "Layanan tidak berjalan"), - ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), - ("Control Remote Desktop", "Kontrol Remote Desktop"), - ("Transfer file", "File Transfer"), - ("Connect", "Hubungkan"), + ("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"), + ("Control Remote Desktop", "Kontrol PC dari jarak jauh"), + ("Transfer file", "Transfer File"), + ("Connect", "Sambungkan"), ("Recent sessions", "Sesi Terkini"), ("Address book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), ("TCP tunneling", "Tunneling TCP"), ("Remove", "Hapus"), ("Refresh random password", "Perbarui kata sandi acak"), - ("Set your own password", "Tetapkan kata sandi Anda"), + ("Set your own password", "Tetapkan kata sandi"), ("Enable keyboard/mouse", "Aktifkan Keyboard/Mouse"), ("Enable clipboard", "Aktifkan Papan Klip"), ("Enable file transfer", "Aktifkan Transfer file"), @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), - ("Your new ID", "ID baru anda"), + ("Your new ID", "ID baru"), ("length %min% to %max%", "panjang %min% s/d %max%"), ("starts with a letter", "Dimulai dengan huruf"), ("allowed characters", "Karakter yang dapat digunakan"), @@ -69,10 +69,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Coba lagi"), ("OK", "Oke"), ("Password Required", "Kata sandi tidak boleh kosong"), - ("Please enter your password", "Silahkan masukkan kata sandi anda"), + ("Please enter your password", "Silahkan masukkan kata sandi"), ("Remember password", "Ingat kata sandi"), ("Wrong Password", "Kata sandi Salah"), - ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), + ("Do you want to enter again?", "Apakah kamu ingin mencoba lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Direset oleh rekan"), @@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "Batalkan Pilihan Semua"), ("Empty Directory", "Folder Kosong"), ("Not an empty directory", "Folder tidak kosong"), - ("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"), - ("Are you sure you want to delete this empty directory?", "Apakah anda yakin untuk menghapus folder ini?"), - ("Are you sure you want to delete the file of this directory?", "Apakah anda yakin untuk menghapus file dan folder ini?"), + ("Are you sure you want to delete this file?", "Apakah kamu yakin untuk menghapus file ini?"), + ("Are you sure you want to delete this empty directory?", "Apakah yakin yakin untuk menghapus folder ini?"), + ("Are you sure you want to delete the file of this directory?", "Apakah yakin yakin untuk menghapus file dan folder ini?"), ("Do this for all conflicts", "Lakukan untuk semua konflik"), ("This is irreversible!", "Ini tidak dapat diubah!"), ("Deleting", "Menghapus"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Tampilkan kualitas monitor"), ("Disable clipboard", "Matikan papan klip"), ("Lock after session end", "Kunci setelah sesi berakhir"), - ("Insert", "Menyisipkan"), + ("Insert Ctrl + Alt + Del", "Menyisipkan Ctrl + Alt + Del"), ("Insert Lock", "Masukkan Kunci"), ("Refresh", "Segarkan"), ("ID does not exist", "ID tidak ada"), @@ -150,20 +150,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Klik untuk unduh"), ("Click to update", "Klik untuk memperbarui"), ("Configure", "Konfigurasi"), - ("config_acc", "Untuk mengontrol Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Aksesibilitas\" RustDesk."), - ("config_screen", "Untuk mengakses Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Perekaman Layar\" RustDesk."), + ("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."), ("Installing ...", "Menginstall"), ("Install", "Instal"), ("Installation", "Instalasi"), ("Installation Path", "Direktori Instalasi"), ("Create start menu shortcuts", "Buat pintasan start menu"), ("Create desktop icon", "Buat icon desktop"), - ("agreement_tip", "Dengan memulai instalasi, Anda menerima perjanjian lisensi."), + ("agreement_tip", "Dengan memulai proses instalasi, Kamu menerima perjanjian lisensi."), ("Accept and Install", "Terima dan Install"), ("End-user license agreement", "Perjanjian lisensi pengguna"), ("Generating ...", "Memproses..."), - ("Your installation is lower version.", "Instalasi Anda adalah versi yang lebih rendah."), - ("not_close_tcp_tip", "Jangan tutup jendela ini saat menggunakan tunnel"), + ("Your installation is lower version.", "Kamu menggunakan versi instalasi yang lebih rendah."), + ("not_close_tcp_tip", "Pastikan jendela ini tetap terbuka saat menggunakan tunnel."), ("Listening ...", "Menghubungkan..."), ("Remote Host", "Host Remote"), ("Remote Port", "Port Remote"), @@ -172,24 +172,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Port Lokal"), ("Local Address", "Alamat lokal"), ("Change Local Port", "Ubah Port Lokal"), - ("setup_server_tip", "Untuk mendapatkan koneksi yang lebih baik, disarankan untuk menginstal di server anda sendiri"), + ("setup_server_tip", "Untuk koneksi yang lebih baik, silakan konfigurasi di server pribadi"), ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."), ("The confirmation is not identical.", "Konfirmasi tidak identik."), ("Permissions", "Perizinan"), ("Accept", "Terima"), ("Dismiss", "Hentikan"), ("Disconnect", "Terputus"), - ("Enable file copy and paste", "Izinkan salin dan tempel file"), + ("Enable file copy and paste", "Izinkan copy dan paste"), ("Connected", "Terhubung"), ("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"), ("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"), ("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"), ("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"), ("Enter Remote ID", "Masukkan ID Remote"), - ("Enter your password", "Masukkan kata sandi anda"), + ("Enter your password", "Masukkan kata sandi"), ("Logging in...", "Masuk..."), ("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"), - ("Auto Login", "Login Otomatis (Hanya berlaku jika Anda mengatur \"Kunci setelah sesi berakhir\")"), + ("Auto Login", "Login Otomatis (Hanya berlaku jika sudah mengatur \"Kunci setelah sesi berakhir\")"), ("Enable direct IP access", "Aktifkan Akses IP Langsung"), ("Rename", "Ubah nama"), ("Space", "Spasi"), @@ -199,7 +199,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Silahkan masukkan nama folder"), ("Fix it", "Perbaiki"), ("Warning", "Peringatan"), - ("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"), + ("Login screen using Wayland is not supported", "Login screen dengan Wayland tidak didukung"), ("Reboot required", "Diperlukan boot ulang"), ("Unsupported display server", "Server tampilan tidak didukung "), ("x11 expected", "Diperlukan x11"), @@ -241,11 +241,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Proksi Socks5"), ("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"), ("Discovered", "Telah ditemukan"), - ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."), + ("install_daemon_tip", "Untuk dapat berjalan saat sistem menyala, kamu perlu menginstal layanan sistem (system service/daemon)."), ("Remote ID", "ID Remote"), ("Paste", "Tempel"), ("Paste here?", "Tempel disini?"), - ("Are you sure to close the connection?", "Apakah anda yakin akan menutup koneksi?"), + ("Are you sure to close the connection?", "Apakah kamu yakin akan menutup koneksi?"), ("Download new version", "Unduh versi baru"), ("Touch mode", "Mode Layar Sentuh"), ("Mouse mode", "Mode Mouse"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Perekaman"), ("Directory", "Direktori"), ("Automatically record incoming sessions", "Otomatis merekam sesi masuk"), + ("Automatically record outgoing sessions", ""), ("Change", "Ubah"), ("Start session recording", "Mulai sesi perekaman"), ("Stop session recording", "Hentikan sesi perekaman"), @@ -373,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Tulis pesan"), ("Prompt", ""), ("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"), - ("elevated_foreground_window_tip", "Jendela remote desktop ini memerlukan hak akses khusus, jadi anda tidak bisa menggunakan mouse dan keyboard untuk sementara. Anda bisa meminta pihak pengguna yang diremote untuk menyembunyikan jendela ini atau klik tombol elevasi di jendela pengaturan koneksi. Untuk menghindari masalah ini, direkomendasikan untuk menginstall aplikasi secara permanen"), + ("elevated_foreground_window_tip", "Jendela yang sedang aktif di remote desktop memerlukan hak istimewa yang lebih tinggi untuk beroperasi, sehingga mouse dan keyboard tidak dapat digunakan sementara waktu. Kamu bisa meminta pengguna jarak jauh untuk meminimalkan jendela saat ini, atau klik tombol elevasi di jendela manajemen koneksi. Untuk menghindari masalah ini, disarankan untuk menginstal software di perangkat remote secara permanen."), ("Disconnected", "Terputus"), ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), @@ -394,9 +395,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Izinkan sesi dengan klik"), ("Accept sessions via both", "Izinkan sesi dengan keduanya"), ("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."), - ("One-time Password", "Kata sandi sekali pakai"), - ("Use one-time password", "Gunakan kata sandi sekali pakai"), - ("One-time password length", "Panjang kata sandi sekali pakai"), + ("One-time Password", "Kata sandi sementara"), + ("Use one-time password", "Gunakan kata sandi sementara"), + ("One-time password length", "Panjang kata sandi sementara"), ("Request access to your device", "Permintaan akses ke perangkat ini"), ("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"), ("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"), @@ -569,67 +570,91 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter privacy mode", "Masuk mode privasi"), ("Exit privacy mode", "Keluar mode privasi"), ("idd_not_support_under_win10_2004_tip", "Driver grafis yang Anda gunakan tidak kompatibel dengan versi Windows Anda dan memerlukan Windows 10 versi 2004 atau yang lebih baru"), - ("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", ""), + ("input_source_1_tip", "Sumber input 1"), + ("input_source_2_tip", "Sumber input 2"), + ("Swap control-command key", "Menukar tombol control-command"), + ("swap-left-right-mouse", "Tukar fungsi tombol kiri dan kanan pada mouse"), + ("2FA code", "Kode 2FA"), + ("More", "Lainnya"), + ("enable-2fa-title", "Aktifkan autentikasi 2FA"), + ("enable-2fa-desc", "Silakan atur autentikator Anda sekarang. Anda dapat menggunakan aplikasi autentikator seperti Authy, Microsoft Authenticator, atau Google Authenticator di ponsel atau desktop Anda\n\nPindai kode QR dengan aplikasi Anda dan masukkan kode yang ditampilkan oleh aplikasi untuk mengaktifkan autentikasi 2FA."), + ("wrong-2fa-code", "Tidak dapat memverifikasi kode. Pastikan bahwa kode dan pengaturan waktu lokal sudah sesuai"), + ("enter-2fa-title", "Autentikasi dua faktor"), + ("Email verification code must be 6 characters.", "Kode verifikasi email harus terdiri dari 6 karakter."), + ("2FA code must be 6 digits.", "Kode 2FA harus terdiri dari 6 digit."), + ("Multiple Windows sessions found", "Terdapat beberapa sesi Windows"), + ("Please select the session you want to connect to", "Silakan pilih sesi yang ingin Anda sambungkan."), + ("powered_by_me", "Didukung oleh RustDesk"), + ("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."), + ("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."), + ("Security Alert", "Peringatan Keamanan"), + ("My address book", "Daftar Kontak"), + ("Personal", "Personal"), + ("Owner", "Pemilik"), + ("Set shared password", "Atus kata sandi kolaboratif"), + ("Exist in", "Ada di"), ("Read-only", ""), ("Read/Write", ""), ("Full Control", ""), - ("share_warning_tip", ""), + ("share_warning_tip", "Informasi di atas bersifat publik dan dapat dilihat oleh orang lain."), ("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", ""), + ("ab_web_console_tip", "Detail Lain di Konsol Web"), + ("allow-only-conn-window-open-tip", "Koneksi hanya diperbolehkan jika jendela RustDesk sedang terbuka."), + ("no_need_privacy_mode_no_physical_displays_tip", "Karena tidak ada layar fisik, mode privasi tidak perlu diaktifkan."), + ("Follow remote cursor", "Ikuti kursor yang terhubung"), + ("Follow remote window focus", "Ikuti jendela remote yang sedang aktif"), + ("default_proxy_tip", "Pengaturan standar untuk protokol dan port adalah Socks5 dan 1080."), + ("no_audio_input_device_tip", "Perangkat input audio tidak terdeteksi."), ("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", ""), + ("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"), + ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Kamu bisa memilih kembali layar untuk dibagi"), + ("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"), + ("android_new_voice_call_tip", "Kamu mendapatkan permintaan panggilan suara baru. Jika diterima, audio akan berubah menjadi komunikasi suara."), + ("texture_render_tip", "Aktifkan rendering tekstur untuk membuat tampilan gambar lebih mulus. Kamu dapat menonaktifkan opsi ini jika terjadi masalah saat merender."), + ("Use texture rendering", "Aktifkan rendering tekstur"), ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), + ("floating_window_tip", "Untuk menjaga layanan/service RustDesk agar tetap aktif"), + ("Keep screen on", "Biarkan layar tetap menyala"), + ("Never", "Tidak pernah"), + ("During controlled", "Dalam proses pengendalian"), ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Capture screen using DirectX", "Rekam layar dengan DirectX"), + ("Back", "Kembali"), + ("Apps", "App"), + ("Volume up", "Naikkan volume"), + ("Volume down", "Turunkan volume"), ("Power", ""), ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("enable-bot-tip", "Jika fitur ini diaktifkan, Kamu dapat menerima kode 2FA dari bot, serta mendapatkan notifikasi tentang koneksi."), + ("enable-bot-desc", "1. Buka chat dengan @BotFather.\n2. Kirim perintah \"/newbot\". Setelah menyelesaikan langkah ini, Kamu akan mendapatkan token\n3. Mulai percakapan dengan bot yang baru dibuat. Kirim pesan yang dimulai dengan garis miring (\"/\") seperti \"/hello\" untuk mengaktifkannya."), + ("cancel-2fa-confirm-tip", "Apakah Kamu yakin ingin membatalkan 2FA?"), + ("cancel-bot-confirm-tip", "Apakah Kamu yakin ingin membatalkan bot Telegram?"), + ("About RustDesk", "Tentang RustDesk"), + ("Send clipboard keystrokes", "Kirim keystrokes clipboard"), + ("network_error_tip", "Periksa koneksi internet, lalu klik \"Coba lagi\"."), + ("Unlock with PIN", "Buka menggunakan PIN"), + ("Requires at least {} characters", "Memerlukan setidaknya {} karakter."), + ("Wrong PIN", "PIN salah"), + ("Set PIN", "Atur PIN"), + ("Enable trusted devices", "Izinkan perangkat tepercaya"), + ("Manage trusted devices", "Kelola perangkat tepercaya"), + ("Platform", "Platform"), + ("Days remaining", "Sisa hari"), + ("enable-trusted-devices-tip", "Tidak memerlukan verifikasi 2FA pada perangkat tepercaya."), + ("Parent directory", "Direktori utama"), + ("Resume", "Lanjutkan"), + ("Invalid file name", "Nama file tidak valid"), + ("one-way-file-transfer-tip", "Transfer file satu arah (One-way) telah diaktifkan pada sisi yang dikendalikan."), + ("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", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 5231ddac48a5..0f6657bbc349 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"), @@ -46,7 +46,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Info programma"), ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), - ("Mute", "Audio off"), + ("Mute", "Audio disabilitato"), ("Build Date", "Data build"), ("Version", "Versione"), ("Home", "Home"), @@ -73,7 +73,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember password", "Ricorda password"), ("Wrong Password", "Password errata"), ("Do you want to enter again?", "Vuoi riprovare?"), - ("Connection Error", "Errore di connessione"), + ("Connection Error", "Errore connessione"), ("Error", "Errore"), ("Reset by the peer", "Reimpostata dal dispositivo remoto"), ("Connecting...", "Connessione..."), @@ -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"), @@ -130,18 +130,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), - ("Insert", "Inserisci"), + ("Insert Ctrl + Alt + Del", "Inserisci Ctrl + Alt + Del"), ("Insert Lock", "Blocco inserimento"), ("Refresh", "Aggiorna"), ("ID does not exist", "L'ID non esiste"), - ("Failed to connect to rendezvous server", "Errore di connessione al server rendezvous"), + ("Failed to connect to rendezvous server", "Errore connessione al server rendezvous"), ("Please try later", "Riprova più tardi"), ("Remote desktop is offline", "Il desktop remoto è offline"), ("Key mismatch", "La chiave non corrisponde"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Errore di connessione al server relay"), - ("Failed to connect via rendezvous server", "Errore di connessione tramite il server rendezvous"), - ("Failed to connect via relay server", "Errore di connessione tramite il server relay"), + ("Failed to connect to relay server", "Errore connessione al server relay"), + ("Failed to connect via rendezvous server", "Errore connessione tramite il server rendezvous"), + ("Failed to connect via relay server", "Errore connessione tramite il server relay"), ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Set Password", "Imposta password"), ("OS Password", "Password sistema operativo"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username", "Nome utente"), ("Invalid port", "Numero porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal dispositivo remoto"), - ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), + ("Enable remote configuration modification", "Abilita modifica remota configurazione"), ("Run without install", "Esegui senza installare"), ("Connect via relay", "Collegati tramite relay"), ("Always connect via relay", "Collegati sempre tramite relay"), @@ -297,7 +297,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"), @@ -363,13 +363,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Sblocca barra strumenti"), ("Recording", "Registrazione"), ("Directory", "Cartella"), - ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), + ("Automatically record incoming sessions", "Registra automaticamente sessioni in entrata"), + ("Automatically record outgoing sessions", "Registra automaticamente sessioni in uscita"), ("Change", "Modifica"), ("Start session recording", "Inizia registrazione sessione"), ("Stop session recording", "Ferma registrazione sessione"), ("Enable recording session", "Abilita registrazione sessione"), ("Enable LAN discovery", "Abilita rilevamento LAN"), - ("Deny LAN discovery", "Nega rilevamento LAN"), + ("Deny LAN discovery", "Non effettuare rilevamento LAN"), ("Write a message", "Scrivi un messaggio"), ("Prompt", "Richiedi"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), @@ -478,7 +479,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"), @@ -532,7 +533,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("HSV Color", "Colore HSV"), ("Installation Successful!", "Installazione completata"), ("Installation failed!", "Installazione fallita"), - ("Reverse mouse wheel", "Rotella mouse inversa"), + ("Reverse mouse wheel", "Funzione rotellina mouse inversa"), ("{} sessions", "{} sessioni"), ("scam_title", "Potresti essere stato TRUFFATO!"), ("scam_text1", "Se sei al telefono con qualcuno che NON conosci NON DI TUA FIDUCIA che ti ha chiesto di usare RustDesk e di avviare il servizio, non procedere e riattacca subito."), @@ -541,25 +542,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("I Agree", "Accetto"), ("Decline", "Non accetto"), ("Timeout in minutes", "Timeout in minuti"), - ("auto_disconnect_option_tip", "Chiudi automaticamente sessioni in entrata in caso di inattività utente"), + ("auto_disconnect_option_tip", "Chiudi automaticamente sessioni in entrata per inattività utente"), ("Connection failed due to inactivity", "Connessione non riuscita a causa di inattività"), ("Check for software update on startup", "All'avvio verifica presenza aggiornamenti programma"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Aggiorna RustDesk Server Pro alla versione {} o successiva!"), ("pull_group_failed_tip", "Impossibile aggiornare il gruppo"), ("Filter by intersection", "Filtra per incrocio"), - ("Remove wallpaper during incoming sessions", "Rimuovi lo sfondo durante le sessioni in entrata"), + ("Remove wallpaper during incoming sessions", "Rimuovi sfondo durante sessioni in entrata"), ("Test", "Test"), ("display_is_plugged_out_msg", "Lo schermo è scollegato, passo al primo schermo."), ("No displays", "Nessuno schermo"), ("Open in new window", "Apri in una nuova finestra"), ("Show displays as individual windows", "Visualizza schermi come finestre individuali"), - ("Use all my displays for the remote session", "Usa tutti gli schermi per la sessione remota"), + ("Use all my displays for the remote session", "Nella sessione remota usa tutti gli schermi"), ("selinux_tip", "In questo dispositivo è abilitato SELinux, che potrebbe impedire il corretto funzionamento di RustDesk come lato controllato."), ("Change view", "Modifica vista"), ("Big tiles", "Icone grandi"), ("Small tiles", "Icone piccole"), ("List", "Elenco"), - ("Virtual display", "Scehrmo virtuale"), + ("Virtual display", "Schermo virtuale"), ("Plug out all", "Scollega tutto"), ("True color (4:4:4)", "Colore reale (4:4:4)"), ("Enable blocking user input", "Abilita blocco input utente"), @@ -598,7 +599,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "I campi sopra indicati sono condivisi e visibili ad altri."), ("Everyone", "Everyone"), ("ab_web_console_tip", "Altre info sulla console web"), - ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), + ("allow-only-conn-window-open-tip", "Consenti connessione solo se la finestra RustDesk è aperta"), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), ("Follow remote cursor", "Segui cursore remoto"), ("Follow remote window focus", "Segui focus finestra remota"), @@ -624,12 +625,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "Volume +"), ("Volume down", "Volume -"), ("Power", "Alimentazione"), - ("Telegram bot", "Bot Telgram"), - ("enable-bot-tip", "If you enable this feature, you can receive the 2FA code from your bot. It can also function as a connection notification."), + ("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?"), - ("cancel-bot-confirm-tip", "Sei sicuro di voler annulare Telegram?"), + ("cancel-bot-confirm-tip", "Sei sicuro di voler annullare Telegram?"), ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), + ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), + ("Unlock with PIN", "Abilita sblocco con PIN"), + ("Requires at least {} characters", "Richiede almeno {} caratteri"), + ("Wrong PIN", "PIN errato"), + ("Set PIN", "Imposta PIN"), + ("Enable trusted devices", "Abilita dispositivi attendibili"), + ("Manage trusted devices", "Gestisci dispositivi attendibili"), + ("Platform", "Piattaforma"), + ("Days remaining", "Giorni rimanenti"), + ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), + ("Parent directory", "Cartella principale"), + ("Resume", "Riprendi"), + ("Invalid file name", "Nome file non valido"), + ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), + ("Authentication Required", "Richiesta autenticazione"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), + ("Download", "Download"), + ("Upload folder", "Cartella upload"), + ("Upload files", "File upload"), + ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), + ("Update client clipboard", "Aggiorna appunti client"), + ("Untagged", "Senza tag"), + ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a1413f82b892..3a967afc6c95 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -19,7 +19,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "最近のセッション"), ("Address book", "アドレス帳"), ("Confirmation", "確認"), - ("TCP tunneling", "TXPトンネリング"), + ("TCP tunneling", "TCPトンネリング"), ("Remove", "削除"), ("Refresh random password", "ランダムパスワードを再生成"), ("Set your own password", "パスワードを設定"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "品質モニターを表示"), ("Disable clipboard", "クリップボードを無効化"), ("Lock after session end", "セッション終了後にロックする"), - ("Insert", "送信"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 送信"), ("Insert Lock", "ロック命令を送信"), ("Refresh", "更新"), ("ID does not exist", "IDが存在しません"), @@ -364,6 +364,7 @@ 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", "セッションの録画を停止"), @@ -430,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length>=8", "8文字以上"), ("Weak", "脆弱"), ("Medium", "普通"), - ("Strong", "協力"), + ("Strong", "強力"), ("Switch Sides", "接続方向の切り替え"), ("Please confirm if you want to share your desktop?", "デスクトップの共有を許可しますか?"), ("Display", "ディスプレイ"), @@ -630,6 +631,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "本当に二要素認証をキャンセルしますか?"), ("cancel-bot-confirm-tip", "本当にTelegram Botをキャンセルしますか?"), ("About RustDesk", "RustDeskについて"), - ("Send clipboard keystrokes", ""), + ("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", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 48b39b4a7081..6a2815aceac7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "품질 모니터 보기"), ("Disable clipboard", "클립보드 비활성화"), ("Lock after session end", "세션 종료 후 화면 잠금"), - ("Insert", "입력"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 입력"), ("Insert Lock", "원격 입력 잠금"), ("Refresh", "새로고침"), ("ID does not exist", "ID가 존재하지 않습니다"), @@ -341,11 +341,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("General", "일반"), ("Security", "보안"), ("Theme", "테마"), - ("Dark Theme", "다크 테마"), - ("Light Theme", "라이트 테마"), - ("Dark", "다크"), - ("Light", "라이트"), - ("Follow System", "시스템 설정에따름"), + ("Dark Theme", "어두운 테마"), + ("Light Theme", "밝은 테마"), + ("Dark", "어둡게"), + ("Light", "밝게"), + ("Follow System", "시스템 기본값"), ("Enable hardware codec", "하드웨어 코덱 활성화"), ("Unlock Security Settings", "보안 설정 잠금 해제"), ("Enable audio", "오디오 활성화"), @@ -364,6 +364,7 @@ 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", "세션 녹화 중지"), @@ -585,51 +586,75 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("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", ""), + ("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", "다이렉트X를 사용한 화면 캡처"), + ("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", "핀으로 잠금 해제"), + ("Requires at least {} characters", "최소 {}자 이상 필요합니다."), + ("Wrong PIN", "잘못된 핀"), + ("Set 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", "{} 의 새로운 버전이 출시되었습니다."), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a8f7fde92d75..1f88ff773899 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Сапа мониторын көрсету"), ("Disable clipboard", "Көшіру-тақтасын өшіру"), ("Lock after session end", "Сеш аяқталған соң құлыптау"), - ("Insert", "Кірістіру"), + ("Insert Ctrl + Alt + Del", "Кірістіру Ctrl + Alt + Del"), ("Insert Lock", "Кірістіруді Құлыптау"), ("Refresh", "Жаңарту"), ("ID does not exist", "ID табылмады"), @@ -364,6 +364,7 @@ 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", ""), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 7167663a52a0..33db01f930ad 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rodyti kokybės monitorių"), ("Disable clipboard", "Išjungti mainų sritį"), ("Lock after session end", "Užrakinti pasibaigus seansui"), - ("Insert", "Įdėti"), + ("Insert Ctrl + Alt + Del", "Įdėti Ctrl + Alt + Del"), ("Insert Lock", "Įterpti užraktą"), ("Refresh", "Atnaujinti"), ("ID does not exist", "ID neegzistuoja"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Įrašymas"), ("Directory", "Katalogas"), ("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"), + ("Automatically record outgoing sessions", ""), ("Change", "Keisti"), ("Start session recording", "Pradėti seanso įrašinėjimą"), ("Stop session recording", "Sustabdyti seanso įrašinėjimą"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index b8add4aa86d6..ae9626ff8e88 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rādīt kvalitātes monitoru"), ("Disable clipboard", "Atspējot starpliktuvi"), ("Lock after session end", "Bloķēt pēc sesijas beigām"), - ("Insert", "Ievietot"), + ("Insert Ctrl + Alt + Del", "Ievietot Ctrl + Alt + Del"), ("Insert Lock", "Ievietot Bloķēt"), ("Refresh", "Atsvaidzināt"), ("ID does not exist", "ID neeksistē"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Ierakstīšana"), ("Directory", "Direktorija"), ("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"), + ("Automatically record outgoing sessions", "Automātiski ierakstīt izejošās sesijas"), ("Change", "Mainīt"), ("Start session recording", "Sākt sesijas ierakstīšanu"), ("Stop session recording", "Apturēt sesijas ierakstīšanu"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1. Atveriet tērzēšanu ar @BotFather.\n2. Nosūtiet komandu \"/newbot\". Pēc šīs darbības pabeigšanas jūs saņemsit pilnvaru.\n3. Sāciet tērzēšanu ar jaunizveidoto robotprogrammatūru. Lai to aktivizētu, nosūtiet ziņojumu, kas sākas ar slīpsvītru (\"/\"), piemēram, \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Vai tiešām vēlaties atcelt 2FA?"), ("cancel-bot-confirm-tip", "Vai tiešām vēlaties atcelt Telegram robotu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "Par RustDesk"), + ("Send clipboard keystrokes", "Nosūtīt starpliktuves taustiņsitienus"), + ("network_error_tip", "Lūdzu, pārbaudiet tīkla savienojumu un pēc tam noklikšķiniet uz Mēģināt vēlreiz."), + ("Unlock with PIN", "Atbloķēt ar PIN"), + ("Requires at least {} characters", "Nepieciešamas vismaz {} rakstzīmes"), + ("Wrong PIN", "Nepareizs PIN"), + ("Set PIN", "Iestatīt PIN"), + ("Enable trusted devices", "Iespējot uzticamas ierīces"), + ("Manage trusted devices", "Pārvaldīt uzticamas ierīces"), + ("Platform", "Platforma"), + ("Days remaining", "Atlikušas dienas"), + ("enable-trusted-devices-tip", "Izlaist 2FA verifikāciju uzticamās ierīcēs"), + ("Parent directory", "Vecākdirektorijs"), + ("Resume", "Atsākt"), + ("Invalid file name", "Nederīgs faila nosaukums"), + ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), + ("Authentication Required", "Nepieciešama autentifikācija"), + ("Authenticate", "Autentificēt"), + ("web_id_input_tip", "Varat ievadīt ID tajā pašā serverī, tīmekļa klientā tiešā IP piekļuve netiek atbalstīta.\nJa vēlaties piekļūt ierīcei citā serverī, lūdzu, pievienojiet servera adresi (@?key=), piemēram,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJa vēlaties piekļūt ierīcei publiskajā serverī, lūdzu, ievadiet \"@public\", publiskajam serverim atslēga nav nepieciešama."), + ("Download", "Lejupielādēt"), + ("Upload folder", "Augšupielādēt mapi"), + ("Upload files", "Augšupielādēt failus"), + ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), + ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), + ("Untagged", "Neatzīmēts"), + ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 36269a377084..eb3564b86db2 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis bildekvalitet"), ("Disable clipboard", "Deaktiver utklipstavle"), ("Lock after session end", "Lås etter avsluttet fjernstyring"), - ("Insert", "Sett inn"), + ("Insert Ctrl + Alt + Del", "Sett inn Ctrl + Alt + Del"), ("Insert Lock", "Sett inn lås"), ("Refresh", "Oppdater"), ("ID does not exist", "ID finnes ikke"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opptak"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Ta opp innkommende sesjoner automatisk"), + ("Automatically record outgoing sessions", ""), ("Change", "Rediger"), ("Start session recording", "Start sesjonsopptak"), ("Stop session recording", "Stopp sesjonsopptak"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index a3042b2f2da3..17bce0695191 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -31,8 +31,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID/Relay Server", "ID/Relay Server"), ("Import server config", "Importeer Serverconfiguratie"), ("Export Server Config", "Exporteer Serverconfiguratie"), - ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), - ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), + ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), ("Invalid server configuration", "Ongeldige Serverconfiguratie"), ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), @@ -80,7 +80,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), ("Please try 1 minute later", "Probeer 1 minuut later"), ("Login Error", "Login Fout"), - ("Successful", "Succesvol"), + ("Successful", "Geslaagd"), ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), ("Name", "Naam"), ("Type", "Type"), @@ -130,7 +130,7 @@ 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", "Invoegen"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoegen"), ("Insert Lock", "Vergrendeling Invoegen"), ("Refresh", "Vernieuwen"), ("ID does not exist", "ID bestaat niet"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opnemen"), ("Directory", "Map"), ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Automatically record outgoing sessions", ""), ("Change", "Wissel"), ("Start session recording", "Start de sessieopname"), ("Stop session recording", "Stop de sessieopname"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "Over RustDesk"), + ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), + ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), + ("Unlock with PIN", "Ontgrendelen met PIN"), + ("Requires at least {} characters", "Vereist minstens {} tekens"), + ("Wrong PIN", "Verkeerde PIN-code"), + ("Set PIN", "PIN-code instellen"), + ("Enable trusted devices", "Vertrouwde apparaten inschakelen"), + ("Manage trusted devices", "Vertrouwde apparaten beheren"), + ("Platform", "Platform"), + ("Days remaining", "Resterende dagen"), + ("enable-trusted-devices-tip", "2FA-verificatie overslaan op vertrouwde apparaten"), + ("Parent directory", "Hoofdmap"), + ("Resume", "Hervatten"), + ("Invalid file name", "Ongeldige bestandsnaam"), + ("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."), + ("Download", "Downloaden"), + ("Upload folder", "Map uploaden"), + ("Upload files", "Bestanden uploaden"), + ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), + ("Update client clipboard", "Klembord van client bijwerken"), + ("Untagged", "Ongemarkeerd"), + ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3e3d135a7632..e28430f498e4 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), - ("Insert", "Wyślij"), + ("Insert Ctrl + Alt + Del", "Wyślij Ctrl + Alt + Del"), ("Insert Lock", "Zablokuj zdalne urządzenie"), ("Refresh", "Odśwież"), ("ID does not exist", "ID nie istnieje"), @@ -349,7 +349,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable audio", "Włącz dźwięk"), - ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), + ("Unlock Network Settings", "Odblokuj ustawienia sieciowe"), ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nagrywanie"), ("Directory", "Folder"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), + ("Automatically record outgoing sessions", "Automatycznie nagrywaj sesje wychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), @@ -602,34 +603,58 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Brak fizycznych wyświetlaczy, tryb prywatny nie jest potrzebny."), ("Follow remote cursor", "Podążaj za zdalnym kursorem"), ("Follow remote window focus", "Podążaj za aktywnością zdalnych okien"), - ("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", ""), + ("default_proxy_tip", "Domyślny protokół i port to Socks5 i 1080"), + ("no_audio_input_device_tip", "Nie znaleziono urządzenia audio."), + ("Incoming", "Przychodzące"), + ("Outgoing", "Wychodzące"), + ("Clear Wayland screen selection", "Wyczyść wybór ekranu Wayland"), + ("clear_Wayland_screen_selection_tip", "Po wyczyszczeniu wyboru ekranu, możesz wybrać, który ekran chcesz udostępnić."), + ("confirm_clear_Wayland_screen_selection_tip", "Na pewno wyczyścić wybór ekranu Wayland?"), + ("android_new_voice_call_tip", "Otrzymano nowe żądanie połączenia głosowego. Jeżeli je zaakceptujesz, dźwięk przełączy się na komunikację głosową."), + ("texture_render_tip", "Użyj renderowania tekstur, aby wygładzić zdjęcia. Możesz spróbować wyłączyć tę opcję, jeżeli napotkasz problemy z renderowaniem."), + ("Use texture rendering", "Użyj renderowania tekstur"), + ("Floating window", "Okno pływające"), + ("floating_window_tip", "Pozwala zachować usługę RustDesk w tle"), + ("Keep screen on", "Pozostaw ekran włączony"), + ("Never", "Nigdy"), + ("During controlled", "Podczas sterowania"), + ("During service is on", "Gdy usługa jest uruchomiona"), + ("Capture screen using DirectX", "Przechwytuj ekran używając DirectX"), + ("Back", "Wstecz"), + ("Apps", "Aplikacje"), + ("Volume up", "Głośniej"), + ("Volume down", "Ciszej"), + ("Power", "Zasilanie"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Jeżeli włączysz tę funkcję, możesz otrzymać kod 2FA od swojego bota. Może on również działać jako powiadomienie o połączeniu."), + ("enable-bot-desc", "1. Otwórz czat z @BotFather.\n2. Wyślij polecenie \"/newbot\". Otrzymasz token do po wykonaniu tego kroku.\n3. Rozpocznij czat z nowo utworzonym botem. Wyślij wiadomość zaczynającą się od ukośnika (\"/\"),np. \"/hello\", aby go aktywować.\n"), + ("cancel-2fa-confirm-tip", "Na pewno chcesz anulować 2FA?"), + ("cancel-bot-confirm-tip", "Na pewno chcesz anulować bot Telegram?"), + ("About RustDesk", "O programie"), + ("Send clipboard keystrokes", "Wysyła naciśnięcia klawiszy ze schowka"), + ("network_error_tip", "Sprawdź swoje połączenie sieciowe, następnie kliknij Ponów."), + ("Unlock with PIN", "Odblokuj za pomocą PIN"), + ("Requires at least {} characters", "Wymaga co najmniej {} znaków"), + ("Wrong PIN", "Niewłaściwy PIN"), + ("Set PIN", "Ustaw PIN"), + ("Enable trusted devices", "Włącz zaufane urządzenia"), + ("Manage trusted devices", "Zarządzaj zaufanymi urządzeniami"), + ("Platform", "Platforma"), + ("Days remaining", "Pozostało dni"), + ("enable-trusted-devices-tip", "Omiń weryfikację 2FA dla zaufanych urządzeń"), + ("Parent directory", "Folder nadrzędny"), + ("Resume", "Wznów"), + ("Invalid file name", "Nieprawidłowa nazwa pliku"), + ("one-way-file-transfer-tip", "Jednokierunkowy transfer plików jest włączony po stronie kontrolowanej."), + ("Authentication Required", "Wymagana autoryzacja"), + ("Authenticate", "Uwierzytelnienie"), + ("web_id_input_tip", "Jeśli chcesz uzyskać dostęp do urządzenia na innym serwerze, dodaj adres serwera (@?key=) na przykład, \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJeśli chcesz uzyskać dostęp do urządzenia na serwerze publicznym, wprowadź \"@public\", klucz nie jest wymagany dla serwera publicznego."), + ("Download", "Pobierz"), + ("Upload folder", "Wyślij folder"), + ("Upload files", "Wyślij pliki"), + ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), + ("Update client clipboard", "Uaktualnij schowek klienta"), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0192d1a44e0b..13f829f77afb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear Inserir"), ("Refresh", "Actualizar"), ("ID does not exist", "ID não existente"), @@ -364,6 +364,7 @@ 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", ""), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a7646bdc880..eff01dd5eee9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Exibir monitor de qualidade"), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear computador"), ("Refresh", "Atualizar"), ("ID does not exist", "ID não existe"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Gravando"), ("Directory", "Diretório"), ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Automatically record outgoing sessions", ""), ("Change", "Alterar"), ("Start session recording", "Iniciar gravação da sessão"), ("Stop session recording", "Parar gravação da sessão"), @@ -498,138 +499,162 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("resolution_original_tip", "Resolução original"), ("resolution_fit_local_tip", "Adequar a resolução local"), ("resolution_custom_tip", "Customizar resolução"), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), + ("Collapse toolbar", "Ocultar barra de ferramentas"), + ("Accept and Elevate", "Aceitar e elevar"), + ("accept_and_elevate_btn_tooltip", "Aceitar a conexão e elevar os privilégios do UAC."), + ("clipboard_wait_response_timeout_tip", "Tempo de espera para a resposta da área de transferência expirado."), + ("Incoming connection", "Conexão de entrada"), + ("Outgoing connection", "Conexão de saída"), ("Exit", "Sair"), ("Open", "Abrir"), - ("logout_tip", ""), + ("logout_tip", "Tem certeza que deseja sair?"), ("Service", "Serviço"), ("Start", "Iniciar"), ("Stop", "Parar"), - ("exceed_max_devices", ""), + ("exceed_max_devices", "Você atingiu o número máximo de dispositivos gerenciados."), ("Sync with recent sessions", "Sincronizar com sessões recentes"), ("Sort tags", "Classificar tags"), ("Open connection in new tab", "Abrir conexão em uma nova aba"), ("Move tab to new window", "Mover aba para uma nova janela"), ("Can not be empty", "Não pode estar vazio"), - ("Already exists", ""), + ("Already exists", "Já existe"), ("Change Password", "Alterar senha"), ("Refresh Password", "Atualizar senha"), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), + ("ID", "ID"), + ("Grid View", "Visualização em grade"), + ("List View", "Visualização em lista"), + ("Select", "Selecionar"), + ("Toggle Tags", "Alternar etiquetas"), + ("pull_ab_failed_tip", "Não foi possível atualizar o diretório"), + ("push_ab_failed_tip", "Não foi possível sincronizar o diretório com o servidor"), + ("synced_peer_readded_tip", "Os dispositivos presentes em sessões recentes serão sincronizados com o diretório."), ("Change Color", "Alterar cor"), ("Primary Color", "Cor principal"), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), + ("HSV Color", "Cor HSV"), + ("Installation Successful!", "Instalação bem-sucedida!"), + ("Installation failed!", "A instalação falhou!"), + ("Reverse mouse wheel", "Inverter rolagem do mouse"), ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), + ("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."), + ("Don't show again", "Não mostrar novamente"), ("I Agree", "Eu concordo"), - ("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", ""), + ("Decline", "Recusar"), + ("Timeout in minutes", "Tempo limite em minutos"), + ("auto_disconnect_option_tip", "Encerrar sessões entrantes automaticamente por inatividade do usuário."), + ("Connection failed due to inactivity", "Conexão encerrada automaticamente por inatividade."), + ("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", ""), ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), ("Test", "Teste"), - ("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", ""), + ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), + ("No displays", "Nenhum display encontrado"), + ("Open in new window", "Abrir em uma nova janela"), + ("Show displays as individual windows", "Mostrar as telas como janelas individuais"), + ("Use all my displays for the remote session", "Usar todas as minhas telas para a sessão remota"), + ("selinux_tip", "O SELinux está ativado em seu dispositivo, o que pode impedir que o RustDesk funcione corretamente como dispositivo controlado."), ("Change view", "Alterar visualização"), - ("Big tiles", ""), - ("Small tiles", ""), + ("Big tiles", "Ícones grandes"), + ("Small tiles", "Ícones pequenos"), ("List", "Lista"), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), + ("Virtual display", "Display Virtual"), + ("Plug out all", "Desconectar tudo"), + ("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", ""), - ("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", ""), + ("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."), + ("input_source_1_tip", "Fonte de entrada 1"), + ("input_source_2_tip", "Fonte de entrada 2"), + ("Swap control-command key", "Trocar teclas Control e Comando"), + ("swap-left-right-mouse", "Trocar botões esquerdo e direito do mouse"), + ("2FA code", "Código 2FA"), + ("More", "Mais"), + ("enable-2fa-title", "Habilitar autenticação em duas etapas"), + ("enable-2fa-desc", "Configure seu autenticador agora. Você pode usar um aplicativo de autenticação como Authy, Microsoft ou Google Authenticator em seu telefone ou computador. Escaneie o código QR com seu aplicativo e insira o código mostrado para habilitar a autenticação em duas etapas."), + ("wrong-2fa-code", "Código inválido. Verifique se o código e as configurações de horário estão corretas."), + ("enter-2fa-title", "Autenticação em duas etapas"), + ("Email verification code must be 6 characters.", "O código de verificação por e-mail deve ter 6 caracteres."), + ("2FA code must be 6 digits.", "O código 2FA deve ter 6 dígitos."), + ("Multiple Windows sessions found", "Múltiplas sessões de janela encontradas"), ("Please select the session you want to connect to", "Por favor, selecione a sessão que você deseja se conectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), + ("powered_by_me", "Desenvolvido por RustDesk"), + ("outgoing_only_desk_tip", "Esta é uma edição personalizada.\nVocê pode se conectar a outros dispositivos, mas eles não podem se conectar ao seu."), + ("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", ""), - ("Owner", ""), + ("Owner", "Proprietário"), ("Set shared password", "Definir senha compartilhada"), ("Exist in", ""), ("Read-only", "Apenas leitura"), ("Read/Write", "Leitura/escrita"), ("Full Control", "Controle total"), - ("share_warning_tip", ""), + ("share_warning_tip", "Os campos mostrados acima são compartilhados e visíveis por outras pessoas."), ("Everyone", "Todos"), - ("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", ""), + ("ab_web_console_tip", "Mais opções no console web"), + ("allow-only-conn-window-open-tip", "Permitir conexões apenas quando a janela do RustDesk estiver aberta"), + ("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", ""), - ("no_audio_input_device_tip", ""), + ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), ("Incoming", ""), ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), + ("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", ""), - ("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", ""), + ("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"), + ("floating_window_tip", "Ajuda a manter o serviço RustDesk em segundo plano"), + ("Keep screen on", "Manter tela ligada"), + ("Never", "Nunca"), + ("During controlled", "Durante controle"), + ("During service is on", "Enquanto o serviço estiver ativo"), + ("Capture screen using DirectX", "Capturar tela usando DirectX"), + ("Back", "Voltar"), + ("Apps", "Apps"), + ("Volume up", "Aumentar volume"), + ("Volume down", "Diminuir volume"), ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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"), + ("cancel-2fa-confirm-tip", "Tem certeza de que deseja cancelar a 2FA?"), + ("cancel-bot-confirm-tip", "Tem certeza de que deseja cancelar o bot do Telegram?"), + ("About RustDesk", "Sobre RustDesk"), + ("Send clipboard keystrokes", "Colar área de transferência"), + ("network_error_tip", "Por favor, verifique sua conexão de rede e clique em tentar novamente."), + ("Unlock with PIN", "Desbloquear com PIN"), + ("Requires at least {} characters", "São necessários pelo menos {} caracteres"), + ("Wrong PIN", "PIN Errado"), + ("Set PIN", "Definir PIN"), + ("Enable trusted devices", "Habilitar dispositivos confiáveis"), + ("Manage trusted devices", "Gerenciar dispositivos confiáveis"), + ("Platform", "Plataforma"), + ("Days remaining", "Dias restantes"), + ("enable-trusted-devices-tip", "Ignore a verificação de dois fatores em dispositivos confiáveis"), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index fba6707317c6..8bd79c189502 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afișează detalii despre conexiune"), ("Disable clipboard", "Dezactivează clipboard"), ("Lock after session end", "Blochează după deconectare"), - ("Insert", "Introdu"), + ("Insert Ctrl + Alt + Del", "Introdu Ctrl + Alt + Del"), ("Insert Lock", "Blochează computer"), ("Refresh", "Reîmprospătează"), ("ID does not exist", "ID neexistent"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Înregistrare"), ("Directory", "Director"), ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifică"), ("Start session recording", "Începe înregistrarea"), ("Stop session recording", "Oprește înregistrarea"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c9a1b41eaf26..b035947d5614 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показывать монитор качества"), ("Disable clipboard", "Отключить буфер обмена"), ("Lock after session end", "Заблокировать учётную запись после сеанса"), - ("Insert", "Вставить"), + ("Insert Ctrl + Alt + Del", "Вставить Ctrl + Alt + Del"), ("Insert Lock", "Заблокировать учётную запись"), ("Refresh", "Обновить"), ("ID does not exist", "ID не существует"), @@ -147,7 +147,7 @@ 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 download", "Нажмите, чтобы скачать"), ("Click to update", "Нажмите, чтобы обновить"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), @@ -364,6 +364,7 @@ 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", "Остановить запись сеанса"), @@ -554,7 +555,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open in new window", "Открыть в новом окне"), ("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"), ("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"), - ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на контролируемой стороне."), + ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на управляемой стороне."), ("Change view", "Вид"), ("Big tiles", "Большие значки"), ("Small tiles", "Маленькие значки"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1) Откройте чат с @BotFather.\n2) Отправьте команду \"/newbot\". После выполнения этого шага вы получите токен.\n3) Начните чат с вашим только что созданным ботом. Отправьте сообщение, начинающееся с прямой косой черты (\"/\"), например, \"/hello\", чтобы его активировать.\n"), ("cancel-2fa-confirm-tip", "Отключить двухфакторную аутентификацию?"), ("cancel-bot-confirm-tip", "Отключить Telegram-бота?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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", "Доступна новая версия {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6d0eb2ac97d9..96c7977cca2d 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobraziť monitor kvality"), ("Disable clipboard", "Vypnúť schránku"), ("Lock after session end", "Po skončení uzamknúť plochu"), - ("Insert", "Vložiť"), + ("Insert Ctrl + Alt + Del", "Vložiť Ctrl + Alt + Del"), ("Insert Lock", "Uzamknúť"), ("Refresh", "Aktualizovať"), ("ID does not exist", "ID neexistuje"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávanie"), ("Directory", "Adresár"), ("Automatically record incoming sessions", "Automaticky nahrávať prichádzajúce relácie"), + ("Automatically record outgoing sessions", ""), ("Change", "Zmeniť"), ("Start session recording", "Spustiť záznam relácie"), ("Stop session recording", "Zastaviť záznam relácie"), @@ -629,7 +630,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Otvorte chat s @BotFather.\n2, Odošlite príkaz \"/newbot\". Po dokončení tohto kroku dostanete token.\n3, Spustite chat s novo vytvoreným botom. Odošlite správu začínajúcu lomítkom vpred (\"/\"), napríklad \"/hello\", aby ste ho aktivovali.\n"), ("cancel-2fa-confirm-tip", "Ste si istí, že chcete zrušiť službu 2FA?"), ("cancel-bot-confirm-tip", "Ste si istí, že chcete zrušiť bota Telegramu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), + ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), + ("Unlock with PIN", "Odomknutie pomocou PIN kódu"), + ("Requires at least {} characters", "Vyžaduje aspoň {} znakov"), + ("Wrong PIN", "Nesprávny PIN kód"), + ("Set PIN", "Nastavenie PIN kódu"), + ("Enable trusted devices", "Povolenie dôveryhodných zariadení"), + ("Manage trusted devices", "Správa dôveryhodných zariadení"), + ("Platform", "Platforma"), + ("Days remaining", "Zostávajúce dni"), + ("enable-trusted-devices-tip", "Vynechanie overovania 2FA na dôveryhodných zariadeniach"), + ("Parent directory", "Rodičovský adresár"), + ("Resume", "Obnoviť"), + ("Invalid file name", "Nesprávny názov súboru"), + ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index ef0c0404bcbe..6cfd29d6c1d9 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"), @@ -37,19 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Odložišče je prazno"), ("Stop service", "Ustavi storitev"), ("Change ID", "Spremeni ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Vaš nov ID"), + ("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."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "Izjava o zasebnosti"), ("Mute", "Izklopi zvok"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Datum graditve"), + ("Version", "Različica"), + ("Home", "Začetek"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), ("Hardware Codec", "Strojni kodek"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži nadzornik kakovosti"), ("Disable clipboard", "Onemogoči odložišče"), ("Lock after session end", "Zakleni ob koncu seje"), - ("Insert", "Vstavi"), + ("Insert Ctrl + Alt + Del", "Vstavi Ctrl + Alt + Del"), ("Insert Lock", "Zakleni oddaljeni računalnik"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne obstaja"), @@ -190,7 +190,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"), @@ -210,27 +210,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Connect via relay", ""), + ("Connect via relay", "Poveži preko posrednika"), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Preveri"), + ("Remember me", "Zapomni si me"), + ("Trust this device", "Zaupaj tej napravi"), + ("Verification code", "Koda za preverjanje"), + ("verification_tip", "Kodo za preverjanje prejmete na registrirani e-poštni naslov"), ("Logout", "Odjavi"), ("Tags", "Oznake"), ("Search ID", "Išči ID"), ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznako"), - ("Unselect all tags", ""), + ("Unselect all tags", "Odznači vse oznake"), ("Network error", "Omrežna napaka"), ("Username missed", "Up. ime izpuščeno"), ("Password missed", "Geslo izpuščeno"), ("Wrong credentials", "Napačne poverilnice"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Koda za preverjanje je napačna, ali pa je potekla"), ("Edit Tag", "Uredi oznako"), ("Forget Password", "Pozabi geslo"), ("Favorites", "Priljubljene"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), ("Download new version", "Prenesi novo različico"), ("Touch mode", "Način dotika"), - ("Mouse mode", "Način mišle"), + ("Mouse mode", "Način miške"), ("One-Finger Tap", "Tap z enim prstom"), ("Left Mouse", "Leva tipka miške"), ("One-Long Tap", "Dolg tap z enim prstom"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tapnite [Zaženi storitev] ali pa omogočite pravico [Zajemanje zaslona] za zagon storitve deljenja zaslona."), + ("android_permission_may_not_change_tip", "Pravic za že vzpostavljene povezave ne morete spremeniti brez ponovne vzpostavitve povezave."), ("Account", "Račun"), ("Overwrite", "Prepiši"), ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), @@ -306,8 +306,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on boot", "Zaženi ob vklopu"), + ("Start the screen sharing service on boot, requires special permissions", "Zaženi storitev deljenja zaslona ob vklopu, zahteva posebna dovoljenja"), ("Connection not allowed", "Povezava ni dovoljena"), ("Legacy mode", "Stari način"), ("Map mode", "Način preslikave"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Razmerje"), ("Image Quality", "Kakovost slike"), ("Scroll Style", "Način drsenja"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Prikaži orodno vrstico"), + ("Hide Toolbar", "Skrij orodno vrstico"), ("Direct Connection", "Neposredna povezava"), ("Relay Connection", "Posredovana povezava"), ("Secure Connection", "Zavarovana povezava"), @@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Varnost"), ("Theme", "Tema"), ("Dark Theme", "Temna tema"), - ("Light Theme", ""), + ("Light Theme", "Svetla tema"), ("Dark", "Temna"), ("Light", "Svetla"), ("Follow System", "Sistemska"), @@ -359,11 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Vhodna naprava za zvok"), ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), ("Network", "Mreža"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Pripni orodno vrstico"), + ("Unpin Toolbar", "Odpni orodno vrstico"), ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Automatically record outgoing sessions", "Samodejno snemaj odhodne seje"), ("Change", "Spremeni"), ("Start session recording", "Začni snemanje seje"), ("Stop session recording", "Ustavi snemanje seje"), @@ -409,227 +410,251 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New 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", ""), - ("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", ""), + ("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", "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"), + ("Ask the remote user for authentication", "Vprašaj oddaljenega uporabnika za prijavo"), + ("Choose this if the remote account is administrator", "Izberite to, če ima oddaljeni uporabnik skrbniške pravice"), + ("Transmit the username and password of administrator", "Vnesite poverilnice za skrbnika"), + ("still_click_uac_tip", "Oddaljeni uporabnik mora klikniti »Da« v oknu za nadzor uporabniškega računa."), + ("Request Elevation", "Zahtevaj povzdig pravic"), + ("wait_accept_uac_tip", "Počakajte na potrditev oddaljenega uporabnika v oknu za nadzor uporabniškega računa."), + ("Elevate successfully", "Povzdig pravic uspešen"), + ("uppercase", "velike črke"), + ("lowercase", "male črke"), + ("digit", "številke"), + ("special character", "posebni znaki"), + ("length>=8", "dolžina>=8"), + ("Weak", "Šibko"), + ("Medium", "Srednje"), + ("Strong", "Močno"), + ("Switch Sides", "Zamenjaj strani"), + ("Please confirm if you want to share your desktop?", "Potrdite, če želite deliti vaše namizje"), + ("Display", "Zaslon"), + ("Default View Style", "Privzeti način prikaza"), + ("Default Scroll Style", "Privzeti način drsenja"), + ("Default Image Quality", "Privzeta kakovost slike"), + ("Default Codec", "Privzeti kodek"), + ("Bitrate", "Bitna hitrost"), + ("FPS", "Sličice/sekundo"), + ("Auto", "Samodejno"), + ("Other Default Options", "Ostale privzete možnosti"), + ("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 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"), + ("No transfers in progress", "Trenutno ni prenosov"), + ("Set one-time password length", "Nastavi dolžino enkratnega gesla"), + ("RDP Settings", "Nastavitve za RDP"), + ("Sort by", "Razvrsti po"), + ("New Connection", "Nova povezava"), + ("Restore", "Obnovi"), + ("Minimize", "Minimiziraj"), + ("Maximize", "Maksimiziraj"), + ("Your Device", "Vaša naprava"), + ("empty_recent_tip", "Oops, ni nedavnih sej.\nPripravite novo."), + ("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"), + ("identical_file_tip", "Datoteka je enaka partnerjevi"), + ("show_monitors_tip", "Prikaži monitorje v orodni vrstici"), + ("View Mode", "Način prikazovanja"), + ("login_linux_tip", "Prijaviti se morate v oddaljeni Linux račun in omogočiti namizno sejo X."), + ("verify_rustdesk_password_tip", "Preveri geslo za RustDesk"), + ("remember_account_tip", "Zapomni si ta račun"), + ("os_account_desk_tip", "Ta račun se uporabi za prijavo v oddaljeni sistem in omogči namizno sejo v napravi brez monitorja."), + ("OS Account", "Račun operacijskega sistema"), + ("another_user_login_title_tip", "Prijavljen je že drug uporabnik"), + ("another_user_login_text_tip", "Prekini"), + ("xorg_not_found_title_tip", "Xorg ni najden"), + ("xorg_not_found_text_tip", "Namestite Xorg"), + ("no_desktop_title_tip", "Namizno okolje ni na voljo"), + ("no_desktop_text_tip", "Namestite GNOME"), + ("No need to elevate", "Povzdig pravic ni potreben"), + ("System Sound", "Sistemski zvok"), + ("Default", "Privzeto"), + ("New RDP", "Nova RDP povezava"), + ("Fingerprint", "Prstni odtis"), + ("Copy Fingerprint", "Kopiraj prstni odtis"), + ("no fingerprints", "ni prstnega odtisa"), + ("Select a peer", "Izberite partnerja"), + ("Select peers", "Izberite partnerje"), + ("Plugins", "Vključki"), + ("Uninstall", "Odstrani"), + ("Update", "Posodobi"), + ("Enable", "Omogoči"), + ("Disable", "Onemogoči"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Izvirna ločljivost"), + ("resolution_fit_local_tip", "Prilagodi lokalni ločljivosti"), + ("resolution_custom_tip", "Ločljivost po meri"), + ("Collapse toolbar", "Strni orodno vrstico"), + ("Accept and Elevate", "Sprejmi in povzdigni pravice"), + ("accept_and_elevate_btn_tooltip", "Sprejmi povezavo in preko nadzora uporabniškera računa povišaj pravice"), + ("clipboard_wait_response_timeout_tip", "Časovna omejitev pri kopiranju je potekla"), + ("Incoming connection", "Dohodna povezava"), + ("Outgoing connection", "Odhodna povezava"), + ("Exit", "Izhod"), + ("Open", "Odpri"), + ("logout_tip", "Ali ste prepričani, da se želite odjaviti?"), + ("Service", "Storitev"), + ("Start", "Zaženi"), + ("Stop", "Ustavi"), + ("exceed_max_devices", "Dosegli ste največje dovoljeno število upravljanih naprav."), + ("Sync with recent sessions", "Sinhroniziraj z nedavnimi sejami"), + ("Sort tags", "Uredi oznake"), + ("Open connection in new tab", "Odpri povezavo na novem zavihku"), + ("Move tab to new window", "Premakni zavihek v novo okno"), + ("Can not be empty", "Ne more biti prazno"), + ("Already exists", "Že obstaja"), + ("Change Password", "Spremeni geslo"), + ("Refresh Password", "Osveži geslo"), + ("ID", "ID"), + ("Grid View", "Mrežni pogled"), + ("List View", "Pogled seznama"), + ("Select", "Izberi"), + ("Toggle Tags", "Preklopi oznake"), + ("pull_ab_failed_tip", "Adresarja ni bilo mogoče osvežiti"), + ("push_ab_failed_tip", "Adresarja ni bilo mogoče poslati na strežnik"), + ("synced_peer_readded_tip", "Naprave, ki so bile prisotne v nedavnih sejah bodo sinhronizirane z adresarjem."), + ("Change Color", "Spremeni barvo"), + ("Primary Color", "Osnovne barve"), + ("HSV Color", "Barve HSV"), + ("Installation Successful!", "Namestitev uspešna"), + ("Installation failed!", "Namestitev ni uspela"), + ("Reverse mouse wheel", "Obrni smer drsenja miškinega kolesca"), + ("{} sessions", "{} sej"), + ("scam_title", "Lahko gre za prevaro!"), + ("scam_text1", "V primeru, da vas je nekdo, ki ga ne poznate in mu zaupate prosil, da uporabite RustDesk, prekinite klic in program zaprite."), + ("scam_text2", "RustDesk omogoča popoln nadzor nad vašim računalnikom in telefonom, in se lahko uporabi za krajo vašega denarja ali pa zasebnih podatkov."), + ("Don't show again", "Ne prikaži znova"), + ("I Agree", "Strinjam se"), + ("Decline", "Zavrni"), + ("Timeout in minutes", "Časovna omejitev v minutah"), + ("auto_disconnect_option_tip", "Samodejno prekini neaktivne seje"), + ("Connection failed due to inactivity", "Povezava je bila prekinjena zaradi neaktivnosti"), + ("Check for software update on startup", "Preveri za posodobitve ob zagonu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Prosimo, nadgradite RustDesk Server Pro na različico {} ali novejšo."), + ("pull_group_failed_tip", "Osveževanje skupine ni uspelo"), + ("Filter by intersection", "Filtriraj po preseku"), + ("Remove wallpaper during incoming sessions", "Odstrani sliko ozadja ob dohodnih povezavah"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Zaslon je bil odklopljen, preklop na primarni zaslon."), + ("No displays", "Ni zaslonov"), + ("Open in new window", "Odpri v novem oknu"), + ("Show displays as individual windows", "Prikaži zaslone kot ločena okna"), + ("Use all my displays for the remote session", "Uporabi vse zaslone za oddaljeno sejo"), + ("selinux_tip", "Na vaši napravi je omogčen SELinux, kar lahko povzroča težave pri oddaljenem nadzoru"), + ("Change view", "Spremeni pogled"), + ("Big tiles", "Velike ploščice"), + ("Small tiles", "Majhne ploščice"), + ("List", "Seznam"), + ("Virtual display", "Navidezni zaslon"), + ("Plug out all", "Odklopi vse"), + ("True color (4:4:4)", "Popolne barve (4:4:4)"), + ("Enable blocking user input", "Omogoči blokiranje vnosa"), + ("id_input_tip", "Vnesete lahko ID, neposredni IP naslov, ali pa domeno in vrata (:)\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.\nČe želite vsiliti povezavo preko posrednika, pripnite »/r« na konec IDja, npr. »9123456234/r«."), + ("privacy_mode_impl_mag_tip", "Način 1"), + ("privacy_mode_impl_virtual_display_tip", "Način 2"), + ("Enter privacy mode", "Vstopi v zasebni način"), + ("Exit privacy mode", "Izstopi iz zasebnega načina"), + ("idd_not_support_under_win10_2004_tip", "Posredni gonilnik ni podprt. Za uporabo rabite Windows 10 2004 ali novejšo različico."), + ("input_source_1_tip", "Vir vnosa 1"), + ("input_source_2_tip", "Vir vnosa 2"), + ("Swap control-command key", "Zamenjaj tipki Ctrl-Command"), + ("swap-left-right-mouse", "Zamenjaj levo in desno tipko miške"), + ("2FA code", "Koda za dvostopenjsko preverjanje"), + ("More", "Več"), + ("enable-2fa-title", "Omogoči dvostopenjsko preverjanje"), + ("enable-2fa-desc", "Pripravite vaš TOTP avtentikator. Uporabite lahko programe kot so Authy, Microsoft ali Google Authenticator, na vašem telefonu ali računalniku.\n\nZa omogočanje dvostopenjskega preverjanja, skenirajte QR kodo in vnesite kodo, ki jo prikaže aplikacija."), + ("wrong-2fa-code", "Kode ni bilo mogoče preveriti. Preverite, da je koda pravilna, in da je nastavitev ure točna."), + ("enter-2fa-title", "Dvostopenjsko preverjanje"), + ("Email verification code must be 6 characters.", "E-poštna koda za preverjanje mora imeti 6 znakov."), + ("2FA code must be 6 digits.", "Koda za dvostopenjsko preverjanje mora imeti 6 znakov."), + ("Multiple Windows sessions found", "Najdenih je bilo več Windows sej"), + ("Please select the session you want to connect to", "Izberite sejo, v katero se želite povezati"), + ("powered_by_me", "Uporablja tehnologijo RustDesk"), + ("outgoing_only_desk_tip", "To je prilagojena različica.\nLahko se povežete na druge naprave, druge naprave pa se k vam ne morejo povezati."), + ("preset_password_warning", "Ta prilagojena različica ima prednastavljeno geslo. Kdorkoli, ki pozna to geslo, lahko prevzame popoln nadzor nad vašim računalnikom. Če tega niste pričakovali, takoj odstranite program."), + ("Security Alert", "Varnostno opozorilo"), + ("My address book", "Moj adresar"), + ("Personal", "Osebni"), + ("Owner", "Lastnik"), + ("Set shared password", "Nastavi deljeno geslo"), + ("Exist in", "Obstaja v"), + ("Read-only", "Samo za branje"), + ("Read/Write", "Branje/pisanje"), + ("Full Control", "Popoln nadzor"), + ("share_warning_tip", "Zgornja polja so deljena, in vidna vsem"), + ("Everyone", "Vsi"), + ("ab_web_console_tip", "Več na spletni konzoli"), + ("allow-only-conn-window-open-tip", "Dovoli povezavo samo če je okno RustDeska odprto"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ni fizičnih zaslonov, zasebni način ni potreben"), + ("Follow remote cursor", "Sledi oddaljenemu kazalcu"), + ("Follow remote window focus", "Sledi oddaljenemu fokusu"), + ("default_proxy_tip", "Privzeti protokol je Socks5 na vratih 1080"), + ("no_audio_input_device_tip", "Ni bilo možno najti vhodne zvočne naprave"), + ("Incoming", "Dohodno"), + ("Outgoing", "Odhodno"), + ("Clear Wayland screen selection", "Počisti izbiro Wayland zaslona"), + ("clear_Wayland_screen_selection_tip", "Po čiščenju izbire Wayland zaslona lahko ponovno izberete zaslon za delitev"), + ("confirm_clear_Wayland_screen_selection_tip", "Ali res želite počistiti izbiro Wayland zaslona?"), + ("android_new_voice_call_tip", "Prejeli ste prošnjo za nov glasovni klic. Če sprejmete, bo zvok preklopljen na glasovno komunikacijo."), + ("texture_render_tip", "Uporabi upodabljanje tekstur, za gladkejše slike. Izklopite, če imate težave pri upodabljanju."), + ("Use texture rendering", "Uporabi upodabljanje tekstur"), + ("Floating window", "Plavajoče okno"), + ("floating_window_tip", "Pomaga pri RustDesk storitvi v ozadju"), + ("Keep screen on", "Ohranite zaslon prižgan"), + ("Never", "Nikoli"), + ("During controlled", "Med nadzorom"), + ("During service is on", "Med vklopljeno storitvijo"), + ("Capture screen using DirectX", "Uporabi DirectX za zajem zaslona"), + ("Back", "Nazaj"), + ("Apps", "Aplikacije"), + ("Volume up", "Glasneje"), + ("Volume down", "Tišje"), + ("Power", "Vklop/izklop"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Če vklopite to možnost, lahko dobite kodo za dvostopenjsko preverjanje od bota. Lahko se uporabi tudi za obveščanje o povezavi."), + ("enable-bot-desc", "1. Odprite pogovor z @BotFather.\n2. Pošljite ukaz »/newbot« in prejeli boste žeton.\n3. Začnite pogovor z na novo narejenim botom. Pošljite sporočilo z desno poševnico (/) kot npr. »/hello« za aktivacijo."), + ("cancel-2fa-confirm-tip", "Ali ste prepričani, da želite ukiniti dvostopenjsko preverjanje?"), + ("cancel-bot-confirm-tip", "Ali ste prepričani, da želite ukiniti Telegram bota?"), + ("About RustDesk", "O RustDesku"), + ("Send clipboard keystrokes", "Vtipkaj vsebino odložišča"), + ("network_error_tip", "Preverite vašo mrežno povezavo, nato kliknite Ponovi."), + ("Unlock with PIN", "Odkleni s PINom"), + ("Requires at least {} characters", "Potrebuje vsaj {} znakov."), + ("Wrong PIN", "Napačen PIN"), + ("Set PIN", "Nastavi PIN"), + ("Enable trusted devices", "Omogoči zaupanja vredne naprave"), + ("Manage trusted devices", "Upravljaj zaupanja vredne naprave"), + ("Platform", "Platforma"), + ("Days remaining", "Preostane dni"), + ("enable-trusted-devices-tip", "Na zaupanja vrednih napravah ni potrebno dvostopenjsko preverjanje"), + ("Parent directory", "Nadrejena mapa"), + ("Resume", "Nadaljuj"), + ("Invalid file name", "Neveljavno ime datoteke"), + ("one-way-file-transfer-tip", "Enosmerni prenos datotek je omogočen na nadzorovani strani"), + ("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", "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 {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 44de8fc20703..ad76f2f9cb28 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Shaq cilësinë e monitorit"), ("Disable clipboard", "Ç'aktivizo clipboard"), ("Lock after session end", "Kyç pasi sesioni të përfundoj"), - ("Insert", "Fut"), + ("Insert Ctrl + Alt + Del", "Fut Ctrl + Alt + Del"), ("Insert Lock", "Fut bllokimin"), ("Refresh", "Rifresko"), ("ID does not exist", "ID nuk ekziston"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Regjistrimi"), ("Directory", "Direktoria"), ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Automatically record outgoing sessions", ""), ("Change", "Ndrysho"), ("Start session recording", "Fillo regjistrimin e sesionit"), ("Stop session recording", "Ndalo regjistrimin e sesionit"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 56b6afb49aed..286658657a06 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži monitor kvaliteta"), ("Disable clipboard", "Zabrani clipboard"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne postoji"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Direktorijum"), ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 0e99407c1f63..fcb2fe1ae419 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visa bildkvalitet"), ("Disable clipboard", "Stäng av urklipp"), ("Lock after session end", "Lås efter sessionens slut"), - ("Insert", "Insert"), + ("Insert Ctrl + Alt + Del", "Insert Ctrl + Alt + Del"), ("Insert Lock", "Insert lås"), ("Refresh", "Uppdatera"), ("ID does not exist", "Detta ID existerar inte"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Spelar in"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"), + ("Automatically record outgoing sessions", ""), ("Change", "Byt"), ("Start session recording", "Starta inspelning"), ("Stop session recording", "Avsluta inspelning"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b1559a65eca0..9f1293ce1985 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", ""), ("Refresh", ""), ("ID does not exist", ""), @@ -364,6 +364,7 @@ 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", ""), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 6237b482245f..83fac2ab8b4f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), - ("Insert", "แทรก"), + ("Insert Ctrl + Alt + Del", "แทรก Ctrl + Alt + Del"), ("Insert Lock", "แทรกล็อค"), ("Refresh", "รีเฟรช"), ("ID does not exist", "ไม่พอข้อมูล ID"), @@ -364,6 +364,7 @@ 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", "หยุดการบันทึกเซสซัน"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e1180ba44d98..630add8bb568 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kalite monitörünü göster"), ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), ("Lock after session end", "Bağlantıdan sonra kilitle"), - ("Insert", "Ekle"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), ("Insert Lock", "Kilit Ekle"), ("Refresh", "Yenile"), ("ID does not exist", "ID bulunamadı"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Kayıt Ediliyor"), ("Directory", "Klasör"), ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"), + ("Automatically record outgoing sessions", ""), ("Change", "Değiştir"), ("Start session recording", "Oturum kaydını başlat"), ("Stop session recording", "Oturum kaydını sonlandır"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 40b9201e15e2..d7eb8dc69fd7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密碼"), ("Ready", "就緒"), ("Established", "已建立"), - ("connecting_status", "正在連線到 RustDesk 網路 ..."), + ("connecting_status", "正在連線到 RustDesk 網路..."), ("Enable service", "啟用服務"), ("Start service", "啟動服務"), ("Service is running", "服務正在執行"), @@ -45,7 +45,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "網站"), ("About", "關於"), ("Slogan_tip", "在這個混沌的世界中用心製作!"), - ("Privacy Statement", "隱私權聲明"), + ("Privacy Statement", "隱私權宣告"), ("Mute", "靜音"), ("Build Date", "建構日期"), ("Version", "版本"), @@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "連線錯誤"), ("Error", "錯誤"), ("Reset by the peer", "對方重設了連線"), - ("Connecting...", "正在連線 ..."), + ("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", "修改時間"), @@ -107,9 +107,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), ("Do this for all conflicts", "套用到其他衝突"), ("This is irreversible!", "此操作不可逆!"), - ("Deleting", "正在刪除 ..."), + ("Deleting", "正在刪除..."), ("files", "檔案"), - ("Waiting", "正在等候 ..."), + ("Waiting", "正在等候..."), ("Finished", "已完成"), ("Speed", "速度"), ("Custom Image Quality", "自訂畫面品質"), @@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始"), ("Shrink", "縮減"), ("Stretch", "延展"), - ("Scrollbar", "滾動條"), - ("ScrollAuto", "自動滾動"), + ("Scrollbar", "捲動條"), + ("ScrollAuto", "自動捲動"), ("Good image quality", "最佳化畫面品質"), ("Balanced", "平衡"), ("Optimize reaction time", "最佳化反應時間"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "顯示品質監測"), ("Disable clipboard", "停用剪貼簿"), ("Lock after session end", "工作階段結束後鎖定電腦"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "鎖定遠端電腦"), ("Refresh", "重新載入"), ("ID does not exist", "ID 不存在"), @@ -150,9 +150,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "點選以下載"), ("Click to update", "點選以更新"), ("Configure", "設定"), - ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「協助工具」權限。"), + ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「無障礙功能」權限。"), ("config_screen", "為了遠端存取您的桌面,您需要授予 RustDesk「螢幕錄製」權限。"), - ("Installing ...", "正在安裝 ..."), + ("Installing ...", "正在安裝..."), ("Install", "安裝"), ("Installation", "安裝"), ("Installation Path", "安裝路徑"), @@ -161,16 +161,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "開始安裝即表示您接受授權條款。"), ("Accept and Install", "接受並安裝"), ("End-user license agreement", "終端使用者授權合約"), - ("Generating ...", "正在產生 ..."), + ("Generating ...", "正在產生..."), ("Your installation is lower version.", "您安裝的版本過舊。"), ("not_close_tcp_tip", "在使用通道時請不要關閉此視窗"), - ("Listening ...", "正在等待通道連線 ..."), + ("Listening ...", "正在等待通道連線..."), ("Remote Host", "遠端主機"), ("Remote Port", "遠端連接埠"), ("Action", "操作"), ("Add", "新增"), ("Local Port", "本機連接埠"), - ("Local Address", "本機地址"), + ("Local Address", "本機位址"), ("Change Local Port", "修改本機連接埠"), ("setup_server_tip", "若您需要更快的連線速度,您可以選擇自行建立伺服器"), ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), @@ -187,8 +187,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "中繼且未加密的連線"), ("Enter Remote ID", "輸入遠端 ID"), ("Enter your password", "輸入您的密碼"), - ("Logging in...", "正在登入 ..."), - ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), + ("Logging in...", "正在登入..."), + ("Enable RDP session sharing", "啟用 RDP 工作階段分享"), ("Auto Login", "自動登入 (只在您設定「工作階段結束後鎖定」時有效)"), ("Enable direct IP access", "啟用 IP 直接存取"), ("Rename", "重新命名"), @@ -216,9 +216,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login", "登入"), ("Verify", "驗證"), ("Remember me", "記住我"), - ("Trust this device", "信任此裝置"), + ("Trust this device", "信任這部裝置"), ("Verification code", "驗證碼"), - ("verification_tip", "驗證碼已發送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"), + ("verification_tip", "驗證碼已傳送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "開機時啟動螢幕分享服務,需要特殊權限。"), ("Connection not allowed", "不允許連線"), ("Legacy mode", "傳統模式"), - ("Map mode", "1:1 傳輸模式"), + ("Map mode", "1:1 傳輸模式"), ("Translate mode", "翻譯模式"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), @@ -329,7 +329,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "顯示設定"), ("Ratio", "比例"), ("Image Quality", "畫質"), - ("Scroll Style", "滾動樣式"), + ("Scroll Style", "捲動樣式"), ("Show Toolbar", "顯示工具列"), ("Hide Toolbar", "隱藏工具列"), ("Direct Connection", "直接連線"), @@ -354,7 +354,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct IP Access", "IP 直接連線"), ("Proxy", "代理伺服器"), ("Apply", "套用"), - ("Disconnect all devices?", "中斷所有遠端連線?"), + ("Disconnect all devices?", "是否中斷所有遠端連線?"), ("Clear", "清空"), ("Audio Input Device", "音訊輸入裝置"), ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), @@ -364,6 +364,7 @@ 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", "停止錄影"), @@ -372,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN discovery", "拒絕區域網路探索"), ("Write a message", "輸入聊天訊息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC..."), ("elevated_foreground_window_tip", "目前遠端桌面的視窗需要更高的權限才能繼續操作,您暫時無法使用滑鼠和鍵盤,您可以請求對方最小化目前視窗,或者在連線管理視窗點選提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), ("Disconnected", "斷開連線"), ("Other", "其他"), @@ -393,7 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "只允許透過輸入密碼進行連線"), ("Accept sessions via click", "只允許透過點選接受進行連線"), ("Accept sessions via both", "允許輸入密碼或點選接受進行連線"), - ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求..."), ("One-time Password", "一次性密碼"), ("Use one-time password", "使用一次性密碼"), ("One-time password length", "一次性密碼長度"), @@ -409,16 +410,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "被 Web 控制台手動關閉"), ("Local keyboard type", "本機鍵盤類型"), ("Select local keyboard type", "請選擇本機鍵盤類型"), - ("software_render_tip", "如果您使用 Nvidia 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), - ("Always use software rendering", "使用軟體渲染"), - ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), - ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), + ("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", "請求遠端使用者進行身分驗證"), + ("Ask the remote user for authentication", "請求遠端使用者進行驗證驗證"), ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), - ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), + ("Transmit the username and password of administrator", "傳送管理員的使用者名稱和密碼"), ("still_click_uac_tip", "依然需要遠端使用者在執行 RustDesk 時於 UAC 視窗點選「是」。"), ("Request Elevation", "請求權限提升"), ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), @@ -435,17 +436,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), ("Display", "顯示"), ("Default View Style", "預設顯示方式"), - ("Default Scroll Style", "預設滾動方式"), - ("Default Image Quality", "預設圖像品質"), + ("Default Scroll Style", "預設捲動方式"), + ("Default Image Quality", "預設影像品質"), ("Default Codec", "預設編解碼器"), ("Bitrate", "位元速率"), - ("FPS", "幀率"), + ("FPS", "FPS"), ("Auto", "自動"), ("Other Default Options", "其他預設選項"), ("Voice call", "語音通話"), ("Text chat", "文字聊天"), ("Stop voice call", "停止語音通話"), - ("relay_hint_tip", "可能無法使用直接連線,您可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,您可以在 ID 後面新增 \"/r\",或是如果近期的工作階段裡存在該設備,您也可以在設備選項裡選擇「一律透過中繼連線」。"), + ("relay_hint_tip", "可能無法使用直接連線,您可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,您可以在 ID 後面新增「/r」,或是如果近期的工作階段裡存在該裝置,您也可以在裝置選項裡選擇「一律透過中繼連線」。"), ("Reconnect", "重新連線"), ("Codec", "編解碼器"), ("Resolution", "解析度"), @@ -526,7 +527,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Toggle Tags", "切換標籤"), ("pull_ab_failed_tip", "通訊錄更新失敗"), ("push_ab_failed_tip", "成功同步通訊錄至伺服器"), - ("synced_peer_readded_tip", "最近會話中存在的設備將會被重新同步到通訊錄。"), + ("synced_peer_readded_tip", "最近工作階段中存在的裝置將會被重新同步到通訊錄。"), ("Change Color", "更改顏色"), ("Primary Color", "基本色"), ("HSV Color", "HSV 色"), @@ -558,17 +559,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change view", "更改檢視方式"), ("Big tiles", "大磁磚"), ("Small tiles", "小磁磚"), - ("List", "列表"), + ("List", "清單"), ("Virtual display", "虛擬螢幕"), ("Plug out all", "拔出所有"), ("True color (4:4:4)", "全彩模式(4:4:4)"), ("Enable blocking user input", "允許封鎖使用者輸入"), - ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+端口號(<網域名稱>:<端口號>)。\n如果您要存取位於其他伺服器上的設備,請在ID之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+連接埠(<網域名稱>:<連接埠>)。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@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", "進入隱私模式"), ("Exit privacy mode", "退出隱私模式"), - ("idd_not_support_under_win10_2004_tip", "不支援 Indirect display driver。 需要 Windows 10 版本 2004 或更新的版本。"), + ("idd_not_support_under_win10_2004_tip", "不支援 Indirect display driver。需要 Windows 10 版本 2004 以上版本。"), ("input_source_1_tip", "輸入源 1"), ("input_source_2_tip", "輸入源 2"), ("Swap control-command key", "交換 Control 和 Command 按鍵"), @@ -576,33 +577,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("2FA code", "二步驟驗證碼"), ("More", "更多"), ("enable-2fa-title", "啟用二步驟驗證"), - ("enable-2fa-desc", "現在請您設定您的二步驟驗證程式。 您可以在手機或電腦使用 Authy、Microsoft 或 Google Authenticator 等驗證器程式。\n\n用它掃描QR Code或輸入下方金鑰至您的驗證器,然後輸入顯示的驗證碼以啟用二步驟驗證。"), - ("wrong-2fa-code", "無法驗證此驗證碼。 請確認您的驗證碼和您的本地時間設置是正確的"), + ("enable-2fa-desc", "現在請您設定您的二步驟驗證程式。您可以在手機或電腦使用 Authy、Microsoft 或 Google Authenticator 等驗證器程式。\n\n用它掃描QR Code或輸入下方金鑰至您的驗證器,然後輸入顯示的驗證碼以啟用二步驟驗證。"), + ("wrong-2fa-code", "無法驗證此驗證碼。請確認您的驗證碼和您的本機時間設定是正確的"), ("enter-2fa-title", "二步驟驗證"), - ("Email verification code must be 6 characters.", "Email 驗證碼必須是 6 個字元。"), + ("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", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即卸載此軟體。"), + ("outgoing_only_desk_tip", "目前版本的軟體是自訂版本。\n您可以連線至其他裝置,但是其他裝置無法連線至您的裝置。"), + ("preset_password_warning", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即移除此軟體。"), ("Security Alert", "安全警告"), ("My address book", "我的通訊錄"), ("Personal", "個人的"), ("Owner", "擁有者"), - ("Set shared password", "設定共享密碼"), + ("Set shared password", "設定共用密碼"), ("Exist in", "存在於"), ("Read-only", "唯讀"), ("Read/Write", "讀寫"), ("Full Control", "完全控制"), - ("share_warning_tip", "上述的欄位為共享且對其他人可見。"), + ("share_warning_tip", "上述的欄位為共用且對其他人可見。"), ("Everyone", "所有人"), - ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), + ("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", "預設代理協定及端口為 Socks5 和 1080"), + ("default_proxy_tip", "預設代理協定及通訊埠為 Socks5 和 1080"), ("no_audio_input_device_tip", "未找到音訊輸入裝置"), ("Incoming", "連入"), ("Outgoing", "連出"), @@ -610,8 +611,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clear_Wayland_screen_selection_tip", "清除 Wayland 的螢幕選擇後,您可以重新選擇分享的螢幕。"), ("confirm_clear_Wayland_screen_selection_tip", "是否確認清除 Wayland 的分享螢幕選擇?"), ("android_new_voice_call_tip", "收到新的語音通話請求。如果您接受,音訊將切換為語音通訊。"), - ("texture_render_tip", "使用紋理渲染,讓圖片更加順暢。 如果您遭遇渲染問題,可嘗試關閉此選項。"), - ("Use texture rendering", "使用紋理渲染"), + ("texture_render_tip", "使用紋理繪製,讓圖片更加順暢。如果您遭遇繪製問題,可嘗試關閉此選項。"), + ("Use texture rendering", "使用紋理繪製"), ("Floating window", "懸浮視窗"), ("floating_window_tip", "有助於保持 RustDesk 後台服務"), ("Keep screen on", "保持螢幕開啟"), @@ -626,10 +627,34 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "電源"), ("Telegram bot", "Telegram 機器人"), ("enable-bot-tip", "如果您啟用此功能,您可以從您的機器人接收二步驟驗證碼,亦可作為連線通知之用。"), - ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令 \"/newbot\"。 您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。 傳送一則以正斜槓 (\"/\") 開頭的訊息來啟用它,例如 \"/hello\"。"), + ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令「/newbot」。您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。傳送一則以正斜線 (「/」) 開頭的訊息來啟用它,例如「/hello」。"), ("cancel-2fa-confirm-tip", "確定要取消二步驟驗證嗎?"), ("cancel-bot-confirm-tip", "確定要取消 Telegram 機器人嗎?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("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,Web 用戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@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", "有新版本的 {} 可用"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/uk.rs similarity index 83% rename from src/lang/ua.rs rename to src/lang/uk.rs index d9dc28fe7983..9f0dfdefbbc8 100644 --- a/src/lang/ua.rs +++ b/src/lang/uk.rs @@ -3,12 +3,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), ("Your Desktop", "Ваша стільниця"), - ("desk_tip", "Ваша стільниця доступна з цим ідентифікатором і паролем"), + ("desk_tip", "Доступ до вашої стільниці можливий з цим ID та паролем."), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Встановлено"), ("connecting_status", "Підключення до мережі RustDesk..."), - ("Enable service", "Включити службу"), + ("Enable service", "Увімкнути службу"), ("Start service", "Запустити службу"), ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), @@ -41,9 +41,9 @@ 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", "Про RustDesk"), + ("About", "Про застосунок"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), @@ -66,7 +66,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Cancel", "Скасувати"), ("Skip", "Пропустити"), ("Close", "Закрити"), - ("Retry", "Спробувати знову"), + ("Retry", "Повторити"), ("OK", "OK"), ("Password Required", "Потрібен пароль"), ("Please enter your password", "Будь ласка, введіть ваш пароль"), @@ -120,9 +120,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Оригінал"), ("Shrink", "Зменшити"), ("Stretch", "Розтягнути"), - ("Scrollbar", "Смуга прокрутки"), - ("ScrollAuto", "Автоматична прокрутка"), - ("Good image quality", "Хороша якість зображення"), + ("Scrollbar", "Смужка гортання"), + ("ScrollAuto", "Автоматичне гортання"), + ("Good image quality", "Гарна якість зображення"), ("Balanced", "Збалансована"), ("Optimize reaction time", "Оптимізувати час реакції"), ("Custom", "Користувацька"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Вимкнути буфер обміну"), ("Lock after session end", "Блокування після завершення сеансу"), - ("Insert", "Вставити"), + ("Insert Ctrl + Alt + Del", "Вставити Ctrl + Alt + Del"), ("Insert Lock", "Встановити замок"), ("Refresh", "Оновити"), ("ID does not exist", "ID не існує"), @@ -145,23 +145,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), ("Set Password", "Встановити пароль"), ("OS Password", "Пароль ОС"), - ("install_tip", "Через UAC в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), + ("install_tip", "Через UAC, в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), ("Click to upgrade", "Натисніть, щоб перевірити наявність оновлень"), - ("Click to download", "Натисніть, щоб завантажити"), + ("Click to download", "Натисніть, щоб отримати"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), - ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Доступності\""), - ("config_screen", "Для віддаленого доступу до вашої стільниці,вам необхідно надати RustDesk дозволи на \"Запис екрана\""), + ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), + ("config_screen", "Для віддаленого доступу до вашої стільниці, вам необхідно надати RustDesk дозволи на \"Запис екрана\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), ("Installation", "Встановлення"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), - ("Create desktop icon", "Створити значок на стільниці"), + ("Create desktop icon", "Створити піктограму на стільниці"), ("agreement_tip", "Починаючи встановлення, ви приймаєте умови ліцензійної угоди"), ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), - ("Generating ...", "Генерація..."), + ("Generating ...", "Генерування..."), ("Your installation is lower version.", "У вас встановлена більш рання версія"), ("not_close_tcp_tip", "Не закривайте це вікно під час використання тунелю"), ("Listening ...", "Очікуємо ..."), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "Локальна адреса"), ("Change Local Port", "Змінити локальний порт"), ("setup_server_tip", "Для пришвидшення зʼєднання, будь ласка, налаштуйте власний сервер"), - ("Too short, at least 6 characters.", "Занадто коротко, мінімум 6 символів"), + ("Too short, at least 6 characters.", "Має бути щонайменше 6 символів"), ("The confirmation is not identical.", "Підтвердження не збігається"), ("Permissions", "Дозволи"), ("Accept", "Прийняти"), @@ -188,8 +188,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter Remote ID", "Введіть віддалений ID"), ("Enter your password", "Введіть пароль"), ("Logging in...", "Вхід..."), - ("Enable RDP session sharing", "Включити загальний доступ до сеансу RDP"), - ("Auto Login", "Автоматичний вхід (дійсний, тільки якщо ви встановили \"Завершення користувацького сеансу після завершення віддаленого підключення\")"), + ("Enable RDP session sharing", "Увімкнути загальний доступ до сеансу RDP"), + ("Auto Login", "Автоматичний вхід (дійсний лише якщо ви встановили \"Блокування після завершення сеансу\")"), ("Enable direct IP access", "Увімкнути прямий IP-доступ"), ("Rename", "Перейменувати"), ("Space", "Місце"), @@ -199,10 +199,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Будь ласка, введіть назву для теки"), ("Fix it", "Виправити"), ("Warning", "Попередження"), - ("Login screen using Wayland is not supported", "Вхід в систему з використанням Wayland не підтримується"), + ("Login screen using Wayland is not supported", "Екран входу, який використовує Wayland, не підтримується"), ("Reboot required", "Потрібне перезавантаження"), ("Unsupported display server", "Графічний сервер не підтримується"), - ("x11 expected", "Очікується X11"), + ("x11 expected", "Потрібен X11"), ("Port", "Порт"), ("Settings", "Налаштування"), ("Username", "Імʼя користувача"), @@ -212,29 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Виявлено новий пристрій, код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), + ("verification_tip", "Код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), ("Logout", "Вийти"), - ("Tags", "Теги"), + ("Tags", "Мітки"), ("Search ID", "Пошук за ID"), - ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), + ("whitelist_sep", "Відокремлення комою, крапкою з комою, пропуском або новим рядком"), ("Add ID", "Додати ID"), - ("Add Tag", "Додати ключове слово"), - ("Unselect all tags", "Скасувати вибір усіх тегів"), + ("Add Tag", "Додати мітку"), + ("Unselect all tags", "Скасувати вибір усіх міток"), ("Network error", "Помилка мережі"), ("Username missed", "Імʼя користувача відсутнє"), ("Password missed", "Пароль відсутній"), ("Wrong credentials", "Неправильні дані"), ("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"), - ("Edit Tag", "Редагувати тег"), + ("Edit Tag", "Редагувати мітку"), ("Forget Password", "Не зберігати пароль"), ("Favorites", "Вибране"), - ("Add to Favorites", "Додати в обране"), + ("Add to Favorites", "Додати до обраного"), ("Remove from Favorites", "Видалити з обраного"), ("Empty", "Пусто"), ("Invalid folder name", "Неприпустима назва теки"), @@ -253,7 +253,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Left Mouse", "Ліва кнопка миші"), ("One-Long Tap", "Одне довге натискання пальцем"), ("Two-Finger Tap", "Дотик двома пальцями"), - ("Right Mouse", "Права миша"), + ("Right Mouse", "Права кнопка миші"), ("One-Finger Move", "Рух одним пальцем"), ("Double Tap & Move", "Подвійне натискання та переміщення"), ("Mouse Drag", "Перетягування мишею"), @@ -279,15 +279,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Connection", "Підключення екрана"), ("Do you accept?", "Ви згодні?"), ("Open System Setting", "Відкрити налаштування системи"), - ("How to get Android input permission?", "Як отримати дозвіл на введення Android?"), - ("android_input_permission_tip1", "Для того, щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або торкання, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), - ("android_input_permission_tip2", "Перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), + ("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_service_will_start_tip", "Увімкнення \"Захоплення екрана\" автоматично запускає службу, дозволяючи іншим пристроям запитувати підключення до вашого пристрою."), ("android_stop_service_tip", "Зупинка служби автоматично завершить всі встановлені зʼєднання."), - ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."), + ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, будь ласка, оновіться до Android 10 або вище."), ("android_start_service_tip", "Натисніть [Запустити службу] або увімкніть дозвіл на [Захоплення екрана], щоб запустити службу спільного доступу до екрана."), - ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не змінитися миттєво аж до перепідключення."), + ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не застосуватися аж до перепідключення."), ("Account", "Обліковий запис"), ("Overwrite", "Перезаписати"), ("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити чи перезаписати файл?"), @@ -307,7 +307,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізації батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Start on boot", "Автозапуск"), - ("Start the screen sharing service on boot, requires special permissions", "Запустити службу службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), + ("Start the screen sharing service on boot, requires special permissions", "Запускати службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), @@ -329,15 +329,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "Налаштування дисплею"), ("Ratio", "Співвідношення"), ("Image Quality", "Якість зображення"), - ("Scroll Style", "Стиль прокрутки"), + ("Scroll Style", "Стиль гортання"), ("Show Toolbar", "Показати панель інструментів"), ("Hide Toolbar", "Приховати панель інструментів"), ("Direct Connection", "Пряме підключення"), ("Relay Connection", "Ретрансльоване підключення"), ("Secure Connection", "Безпечне підключення"), ("Insecure Connection", "Небезпечне підключення"), - ("Scale original", "Оригінал масштабу"), - ("Scale adaptive", "Масштаб адаптивний"), + ("Scale original", "Оригінальний масштаб"), + ("Scale adaptive", "Адаптивний масштаб"), ("General", "Загальні"), ("Security", "Безпека"), ("Theme", "Тема"), @@ -351,7 +351,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), - ("Direct IP Access", "Прямий IP доступ"), + ("Direct IP Access", "Прямий IP-доступ"), ("Proxy", "Проксі"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відʼєднати всі прилади?"), @@ -364,6 +364,7 @@ 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", "Закінчити запис сеансу"), @@ -373,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Написати повідомлення"), ("Prompt", "Підказка"), ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."), - ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використовувати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачеві згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої."), ("Disconnected", "Відʼєднано"), ("Other", "Інше"), ("Confirm before closing multiple tabs", "Підтверджувати перед закриттям кількох вкладок"), @@ -401,7 +402,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Приховати вікно керування підключеннями"), ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), - ("Right click to select tabs", "Правий клік для вибору вкладки"), + ("Right click to select tabs", "Вибір вкладок клацанням правою"), ("Skipped", "Пропущено"), ("Add to address book", "Додати IP до Адресної книги"), ("Group", "Група"), @@ -413,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Завжди використовувати програмну візуалізацію"), ("config_input", "Для віддаленого керування віддаленою стільницею з клавіатури, вам необхідно надати RustDesk дозволи на \"Відстеження введення\""), ("config_microphone", "Для можливості віддаленої розмови, вам необхідно надати RustDesk дозвіл на \"Запис аудіо\""), - ("request_elevation_tip", "Ви також можете надіслати запит на розширення прав, в разі присутності особи з віддаленого боку."), + ("request_elevation_tip", "Ви можете також надіслати запит на розширення прав, в разі присутності особи з віддаленого боку."), ("Wait", "Зачекайте"), ("Elevation Error", "Невдала спроба розширення прав"), ("Ask the remote user for authentication", "Попросіть віддаленого користувача пройти автентифікацію"), @@ -460,8 +461,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Your Device", "Вам пристрій"), ("empty_recent_tip", "Овва, відсутні нещодавні сеанси!\nСаме час запланувати нове підключення."), ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"), - ("empty_lan_tip", "О ні, схоже ми поки не виявили жодного віддаленого пристрою"), - ("empty_address_book_tip", "Ой лишенько, схоже до вашої адресної книги немає жодного віддаленого пристрою"), + ("empty_lan_tip", "О ні, схоже ми ще не виявили жодного віддаленого пристрою."), + ("empty_address_book_tip", "Ой лишенько, схоже у вашій адресній книзі немає жодного віддаленого пристрою."), ("eg: admin", "напр., admin"), ("Empty Username", "Незаповнене імʼя"), ("Empty Password", "Незаповнений пароль"), @@ -469,12 +470,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Цей файл ідентичний з тим, що на вузлі"), ("show_monitors_tip", "Показувати монітори на панелі інструментів"), ("View Mode", "Режим перегляду"), - ("login_linux_tip", "Вам необхідно залогуватися у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), + ("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_title_tip", "Інший користувач вже в системі"), ("another_user_login_text_tip", "Відʼєднатися"), ("xorg_not_found_title_tip", "Xorg не знайдено"), ("xorg_not_found_text_tip", "Будь ласка, встановіть Xorg"), @@ -506,13 +507,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing connection", "Вихідне підключення"), ("Exit", "Вийти"), ("Open", "Відкрити"), - ("logout_tip", "Ви впевнені, що хочете вилогуватися?"), + ("logout_tip", "Ви впевнені, що хочете вийти з системи?"), ("Service", "Служба"), ("Start", "Запустити"), ("Stop", "Зупинити"), ("exceed_max_devices", "У вас максимальна кількість керованих пристроїв."), ("Sync with recent sessions", "Синхронізація з нещодавніми сеансами"), - ("Sort tags", "Сортувати теги"), + ("Sort tags", "Сортувати мітки"), ("Open connection in new tab", "Відкрити підключення в новій вкладці"), ("Move tab to new window", "Перемістити вкладку до нового вікна"), ("Can not be empty", "Не може бути порожнім"), @@ -523,16 +524,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Перегляд ґраткою"), ("List View", "Перегляд списком"), ("Select", "Вибрати"), - ("Toggle Tags", "Видимість тегів"), + ("Toggle Tags", "Видимість міток"), ("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"), ("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"), - ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою"), + ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою."), ("Change Color", "Змінити колір"), ("Primary Color", "Основний колір"), ("HSV Color", "Колір HSV"), ("Installation Successful!", "Успішне встановлення!"), ("Installation failed!", "Невдале встановлення!"), - ("Reverse mouse wheel", "Зворотній напрям прокрутки"), + ("Reverse mouse wheel", "Зворотній напрям гортання"), ("{} sessions", "{} сеансів"), ("scam_title", "Вас можуть ОБМАНУТИ!"), ("scam_text1", "Якщо ви розмовляєте по телефону з кимось, кого НЕ ЗНАЄТЕ чи кому НЕ ДОВІРЯЄТЕ, і ця особа хоче, щоб ви використали RustDesk та запустили службу, не робіть цього та негайно завершіть дзвінок."), @@ -544,7 +545,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Автоматично завершувати вхідні сеанси в разі неактивності користувача"), ("Connection failed due to inactivity", "Зʼєднання розірвано через неактивність"), ("Check for software update on startup", "Перевіряти оновлення під час запуску"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи більш актуальної!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи новіше!"), ("pull_group_failed_tip", "Не вдалося оновити групу"), ("Filter by intersection", "Фільтр за збігом"), ("Remove wallpaper during incoming sessions", "Прибирати шпалеру під час вхідних сеансів"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Відключити все"), ("True color (4:4:4)", "Справжній колір (4:4:4)"), ("Enable blocking user input", "Блокувати введення для користувача"), - ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", ключ для публічного сервера не потрібен."), + ("id_input_tip", "Ви можете ввести ID, безпосередню 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", "Увійти в режим конфіденційності"), @@ -619,17 +620,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Send clipboard keystrokes", ""), + ("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", "Пропускати двофакторну автентифікацію на довірених пристроях"), + ("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", "Доступна нова версія {}"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f68786b836a3..1ee2cd6d026e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Hiện thị chất lượng của màn hình"), ("Disable clipboard", "Tắt clipboard"), ("Lock after session end", "Khóa sau khi kết thúc phiên kết nối"), - ("Insert", "Cài"), + ("Insert Ctrl + Alt + Del", "Cài Ctrl + Alt + Del"), ("Insert Lock", "Cài khóa"), ("Refresh", "Làm mới"), ("ID does not exist", "ID không tồn tại"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Đang ghi hình"), ("Directory", "Thư mục"), ("Automatically record incoming sessions", "Tự động ghi những phiên kết nối vào"), + ("Automatically record outgoing sessions", ""), ("Change", "Thay đổi"), ("Start session recording", "Bắt đầu ghi hình phiên kết nối"), ("Stop session recording", "Dừng ghi hình phiên kết nối"), @@ -631,5 +632,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index f8d917a518e2..693f36dbcd65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ mod keyboard; -#[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -12,7 +11,6 @@ mod server; #[cfg(not(any(target_os = "ios")))] pub use self::server::*; mod client; -#[cfg(not(any(target_os = "ios")))] mod lan; #[cfg(not(any(target_os = "ios")))] mod rendezvous_mediator; @@ -47,7 +45,7 @@ mod custom_server; mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] mod clipboard; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/main.rs b/src/main.rs index bc41365e3fbf..f295363aa90e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ fn main() { } common::test_rendezvous_server(); common::test_nat_type(); - #[cfg(target_os = "android")] - crate::common::check_software_update(); common::global_clean(); } @@ -102,7 +100,7 @@ fn main() { cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { log::info!("id={}", hbb_common::config::Config::get_id()); - crate::start_server(true); + crate::start_server(true, false); } common::global_clean(); } diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs new file mode 100644 index 000000000000..9aeea1e2b01c --- /dev/null +++ b/src/platform/gtk_sudo.rs @@ -0,0 +1,771 @@ +// https://github.com/aarnt/qt-sudo +// Sometimes reboot is needed to refresh sudoers. + +use crate::lang::translate; +use gtk::{glib, prelude::*}; +use hbb_common::{ + anyhow::{bail, Error}, + log, ResultType, +}; +use nix::{ + libc::{fcntl, kill}, + pty::{forkpty, ForkptyResult}, + sys::{ + signal::Signal, + wait::{waitpid, WaitPidFlag}, + }, + unistd::{execvp, setsid, Pid}, +}; +use std::{ + ffi::CString, + fs::File, + io::{Read, Write}, + os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, +}; + +const EXIT_CODE: i32 = -1; + +enum Message { + PasswordPrompt((String, bool)), + Password((String, String)), + ErrorDialog(String), + Cancel, + Exit(i32), +} + +pub fn run(cmds: Vec<&str>) -> ResultType<()> { + // rustdesk service kill `rustdesk --` processes + let second_arg = std::env::args().nth(1).unwrap_or_default(); + let cmd_mode = + second_arg.starts_with("--") && second_arg != "--tray" && second_arg != "--no-server"; + let mod_arg = if cmd_mode { "cmd" } else { "gui" }; + let mut args = vec!["-gtk-sudo", mod_arg]; + args.append(&mut cmds.clone()); + let mut child = crate::run_me(args)?; + let exit_status = child.wait()?; + if exit_status.success() { + Ok(()) + } else { + bail!("child exited with status: {:?}", exit_status); + } +} + +pub fn exec() { + let mut args = vec![]; + for arg in std::env::args().skip(3) { + args.push(arg); + } + let cmd_mode = std::env::args().nth(2) == Some("cmd".to_string()); + if cmd_mode { + cmd(args); + } else { + ui(args); + } +} + +fn cmd(args: Vec) { + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = cmd_parent(child, master) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(None, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + std::process::exit(EXIT_CODE); + } + } +} + +fn ui(args: Vec) { + // https://docs.gtk.org/gtk4/ctor.Application.new.html + // https://docs.gtk.org/gio/type_func.Application.id_is_valid.html + let application = gtk::Application::new(None, Default::default()); + + let (tx_to_ui, rx_to_ui) = channel::(); + let (tx_from_ui, rx_from_ui) = channel::(); + + let rx_to_ui = Arc::new(Mutex::new(rx_to_ui)); + let tx_from_ui = Arc::new(Mutex::new(tx_from_ui)); + + let rx_to_ui_clone = rx_to_ui.clone(); + let tx_from_ui_clone = tx_from_ui.clone(); + + let username = Arc::new(Mutex::new(crate::platform::get_active_username())); + let username_clone = username.clone(); + + application.connect_activate(glib::clone!(@weak application =>move |_| { + let rx_to_ui = rx_to_ui_clone.clone(); + let tx_from_ui = tx_from_ui_clone.clone(); + let last_password = Arc::new(Mutex::new(String::new())); + let username = username_clone.clone(); + + glib::timeout_add_local(std::time::Duration::from_millis(50), move || { + if let Ok(msg) = rx_to_ui.lock().unwrap().try_recv() { + match msg { + Message::PasswordPrompt((err_msg, show_edit)) => { + let last_pwd = last_password.lock().unwrap().clone(); + let username = username.lock().unwrap().clone(); + if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, show_edit) { + *last_password.lock().unwrap() = password.clone(); + if let Err(e) = tx_from_ui + .lock() + .unwrap() + .send(Message::Password((username, password))) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } else { + if let Err(e) = tx_from_ui.lock().unwrap().send(Message::Cancel) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } + } + Message::ErrorDialog(err_msg) => { + error_dialog_and_exit(&err_msg, EXIT_CODE); + } + Message::Exit(code) => { + log::info!("Exit code: {}", code); + std::process::exit(code); + } + _ => {} + } + } + glib::ControlFlow::Continue + }); + })); + + let tx_to_ui_clone = tx_to_ui.clone(); + std::thread::spawn(move || { + let acitve_user = crate::platform::get_active_username(); + let mut initial_password = None; + if acitve_user != "root" { + if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt(("".to_string(), true))) { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + match rx_from_ui.recv() { + Ok(Message::Password((user, password))) => { + *username.lock().unwrap() = user; + initial_password = Some(password); + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + std::process::exit(EXIT_CODE); + } + _ => { + log::error!("Unexpected message"); + std::process::exit(EXIT_CODE); + } + } + } + let username = username.lock().unwrap().clone(); + let su_user = if username == acitve_user { + None + } else { + Some(username) + }; + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = ui_parent( + child, + master, + tx_to_ui_clone, + rx_from_ui, + su_user.is_some(), + initial_password, + ) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(su_user, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + if let Err(e) = + tx_to_ui.send(Message::ErrorDialog(format!("Forkpty error: {:?}", err))) + { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + } + } + }); + + let _holder = application.hold(); + let args: Vec<&str> = vec![]; + application.run_with_args(&args); + log::debug!("exit from gtk::Application::run_with_args"); + std::process::exit(EXIT_CODE); +} + +fn cmd_parent(child: Pid, master: OwnedFd) -> ResultType<()> { + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + let mut stdout = std::io::stdout(); + let stdin = std::io::stdin(); + let stdin_fd = stdin.as_raw_fd(); + let old_termios = termios::Termios::from_fd(stdin_fd)?; + turn_off_echo(stdin_fd).ok(); + shutdown_hooks::add_shutdown_hook(turn_on_echo_shutdown_hook); + let (tx, rx) = channel::>(); + std::thread::spawn(move || loop { + let mut line = String::default(); + match stdin.read_line(&mut line) { + Ok(0) => { + kill_child(child); + break; + } + Ok(_) => { + if let Err(e) = tx.send(line.as_bytes().to_vec()) { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => { + log::info!("Failed to read stdin: {e:?}"); + kill_child(child); + break; + } + }; + }); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + let buf = String::from_utf8_lossy(&buf[..n]).to_string(); + print!("{}", buf); + if let Err(e) = stdout.flush() { + log::error!("flush failed: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::info!("Read child error: {:?}", e); + break; + } + } + match rx.try_recv() { + Ok(v) => { + if let Err(e) = file.write_all(&v) { + log::error!("write error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + log::error!("receive error: {e:?}"); + kill_child(child); + break; + } + }, + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + termios::tcsetattr(stdin_fd, termios::TCSANOW, &old_termios).ok(); + std::process::exit(code); +} + +fn ui_parent( + child: Pid, + master: OwnedFd, + tx_to_ui: Sender, + rx_from_ui: Receiver, + is_su: bool, + initial_password: Option, +) -> ResultType<()> { + let mut initial_password = initial_password; + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + tx_to_ui.send(Message::ErrorDialog(format!("fcntl error: {errno:?}")))?; + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + + let mut first = initial_password.is_none(); + let mut su_password_sent = false; + let mut saved_output = String::default(); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + saved_output = String::default(); + let buf = String::from_utf8_lossy(&buf[..n]).trim().to_string(); + let last_line = buf.lines().last().unwrap_or(&buf).trim().to_string(); + log::info!("read from child: {}", buf); + + if last_line.starts_with("sudo:") || last_line.starts_with("su:") { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(last_line)) { + log::error!("Channel error: {e:?}"); + kill_child(child); + } + break; + } else if last_line.ends_with(":") { + match get_echo_turn_off(raw_fd) { + Ok(true) => { + log::debug!("get_echo_turn_off ok"); + if let Some(password) = initial_password.clone() { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + if is_su && !su_password_sent { + su_password_sent = true; + continue; + } + initial_password = None; + continue; + } + // In fact, su mode can only input password once + let err_msg = if first { "" } else { "Sorry, try again." }; + first = false; + if let Err(e) = + tx_to_ui.send(Message::PasswordPrompt((err_msg.to_string(), false))) + { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + match rx_from_ui.recv() { + Ok(Message::Password((_, password))) => { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + kill_child(child); + break; + } + _ => { + log::error!("Unexpected message"); + break; + } + } + } + Ok(false) => log::warn!("get_echo_turn_off timeout"), + Err(e) => log::error!("get_echo_turn_off error: {:?}", e), + } + } else { + saved_output = buf.clone(); + if !last_line.is_empty() && initial_password.is_some() { + log::error!("received not empty line: {last_line}, clear initial password"); + initial_password = None; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::debug!("Read error: {:?}", e); + break; + } + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + + if code != 0 && !saved_output.is_empty() { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(saved_output.clone())) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + return Ok(()); + } + if let Err(e) = tx_to_ui.send(Message::Exit(code)) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + Ok(()) +} + +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 mut params = vec!["sudo".to_string()]; + if su_user.is_some() { + params.push("-S".to_string()); + } + params.push("/bin/sh".to_string()); + params.push("-c".to_string()); + + let command = args + .iter() + .map(|s| { + if su_user.is_some() { + s.to_string() + } else { + quote_shell_arg(s, true) + } + }) + .collect::>() + .join(" "); + let mut command = if bsd { + let lc = match std::env::var("LC_ALL") { + Ok(lc_all) => { + if lc_all.contains('\'') { + eprintln!( + "sudo: Detected attempt to inject privileged command via LC_ALL env({lc_all}). Exiting!\n", + ); + std::process::exit(EXIT_CODE); + } + format!("LC_ALL='{lc_all}' ") + } + Err(_) => { + format!("unset LC_ALL;") + } + }; + format!("{}exec {}", lc, command) + } else { + command.to_string() + }; + if su_user.is_some() { + command = format!("'{}'", quote_shell_arg(&command, false)); + } + params.push(command); + std::env::set_var("LC_ALL", "C"); + + if let Some(user) = &su_user { + let su_subcommand = params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(" "); + params = vec![ + "su".to_string(), + "-".to_string(), + user.to_string(), + "-c".to_string(), + su_subcommand, + ]; + } + + // allow failure here + let _ = setsid(); + let mut cparams = vec![]; + for param in ¶ms { + cparams.push(CString::new(param.as_str())?); + } + let su_or_sudo = if su_user.is_some() { "su" } else { "sudo" }; + let res = execvp(CString::new(su_or_sudo)?.as_c_str(), &cparams); + eprintln!("sudo: execvp error: {:?}", res); + std::process::exit(EXIT_CODE); +} + +fn get_echo_turn_off(fd: RawFd) -> Result { + let tios = termios::Termios::from_fd(fd)?; + for _ in 0..10 { + if tios.c_lflag & termios::ECHO == 0 { + return Ok(true); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Ok(false) +} + +fn turn_off_echo(fd: RawFd) -> Result<(), Error> { + use termios::*; + let mut termios = Termios::from_fd(fd)?; + // termios.c_lflag &= !(ECHO | ECHONL | ICANON | IEXTEN); + termios.c_lflag &= !ECHO; + tcsetattr(fd, TCSANOW, &termios)?; + Ok(()) +} + +pub extern "C" fn turn_on_echo_shutdown_hook() { + let fd = std::io::stdin().as_raw_fd(); + if let Ok(mut termios) = termios::Termios::from_fd(fd) { + termios.c_lflag |= termios::ECHO; + termios::tcsetattr(fd, termios::TCSANOW, &termios).ok(); + } +} + +fn kill_child(child: Pid) { + unsafe { kill(child.as_raw(), Signal::SIGINT as _) }; + let mut res = 0; + + for _ in 0..10 { + match waitpid(child, Some(WaitPidFlag::WNOHANG)) { + Ok(_) => { + res = 1; + break; + } + Err(_) => (), + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + if res == 0 { + log::info!("Force killing child process"); + unsafe { kill(child.as_raw(), Signal::SIGKILL as _) }; + } +} + +fn password_prompt( + username: &str, + last_password: &str, + err: &str, + show_edit: bool, +) -> Option<(String, String)> { + let dialog = gtk::Dialog::builder() + .title(crate::get_app_name()) + .modal(true) + .build(); + // https://docs.gtk.org/gtk4/method.Dialog.set_default_response.html + dialog.set_default_response(gtk::ResponseType::Ok); + let content_area = dialog.content_area(); + + let label = gtk::Label::builder() + .label(translate("Authentication Required".to_string())) + .margin_top(10) + .build(); + content_area.add(&label); + + let image = gtk::Image::from_icon_name(Some("avatar-default-symbolic"), gtk::IconSize::Dialog); + image.set_margin_top(10); + content_area.add(&image); + + let user_label = gtk::Label::new(Some(username)); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + let edit_icon = + gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button.into()); + edit_button.set_image(Some(&edit_icon)); + edit_button.set_can_focus(false); + let user_entry = gtk::Entry::new(); + user_entry.set_alignment(0.5); + user_entry.set_width_request(100); + let user_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + user_box.add(&user_label); + user_box.add(&edit_button); + user_box.add(&user_entry); + user_box.set_halign(gtk::Align::Center); + user_box.set_valign(gtk::Align::Center); + user_box.set_vexpand(true); + content_area.add(&user_box); + + edit_button.connect_clicked( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry=> move |_| { + let username = user_label.text().to_string(); + user_entry.set_text(&username); + user_label.hide(); + edit_button.hide(); + user_entry.show(); + user_entry.grab_focus(); + }), + ); + + let password_input = gtk::Entry::builder() + .visibility(false) + .input_purpose(gtk::InputPurpose::Password) + .placeholder_text(translate("Password".to_string())) + .margin_top(20) + .margin_start(30) + .margin_end(30) + .activates_default(true) + .text(last_password) + .build(); + password_input.set_alignment(0.5); + // https://docs.gtk.org/gtk3/signal.Entry.activate.html + password_input.connect_activate(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + content_area.add(&password_input); + + user_entry.connect_focus_out_event( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => @default-return glib::Propagation::Proceed, move |_, _| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + glib::Propagation::Proceed + }), + ); + user_entry.connect_activate( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => move |_| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + password_input.grab_focus(); + }), + ); + + if !err.is_empty() { + let err_label = gtk::Label::new(None); + err_label.set_markup(&format!( + "{}", + err + )); + err_label.set_selectable(true); + content_area.add(&err_label); + } + + let cancel_button = gtk::Button::builder() + .label(translate("Cancel".to_string())) + .hexpand(true) + .build(); + cancel_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Cancel); + })); + let authenticate_button = gtk::Button::builder() + .label(translate("Authenticate".to_string())) + .hexpand(true) + .build(); + authenticate_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .hexpand(true) + .homogeneous(true) + .spacing(10) + .margin_top(10) + .build(); + button_box.add(&cancel_button); + button_box.add(&authenticate_button); + content_area.add(&button_box); + + content_area.set_spacing(10); + content_area.set_border_width(10); + + dialog.set_width_request(400); + dialog.show_all(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + password_input.grab_focus(); + user_entry.hide(); + if !show_edit { + edit_button.hide(); + } + dialog.check_resize(); + let response = dialog.run(); + dialog.hide(); + + if response == gtk::ResponseType::Ok { + let username = if user_entry.get_visible() { + user_entry.text().to_string() + } else { + user_label.text().to_string() + }; + Some((username, password_input.text().to_string())) + } else { + None + } +} + +fn error_dialog_and_exit(err_msg: &str, exit_code: i32) { + log::error!("Error dialog: {err_msg}, exit code: {exit_code}"); + let dialog = gtk::MessageDialog::builder() + .message_type(gtk::MessageType::Error) + .title(crate::get_app_name()) + .text("Error") + .secondary_text(err_msg) + .modal(true) + .buttons(gtk::ButtonsType::Ok) + .build(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + dialog.run(); + dialog.close(); + std::process::exit(exit_code); +} + +fn quote_shell_arg(arg: &str, add_splash_if_match: bool) -> String { + let mut rv = arg.to_string(); + let re = hbb_common::regex::Regex::new("(\\s|[][!\"#$&'()*,;<=>?\\^`{}|~])"); + let Ok(re) = re else { + return rv; + }; + if re.is_match(arg) { + rv = rv.replace("'", "'\\''"); + if add_splash_if_match { + rv = format!("'{}'", rv); + } + } + rv +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index f936bfb8cc11..08cf0fb9a901 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,4 +1,4 @@ -use super::{CursorData, ResultType}; +use super::{gtk_sudo, CursorData, ResultType}; use desktop::Desktop; use hbb_common::config::keys::OPTION_ALLOW_LINUX_HEADLESS; pub use hbb_common::platform::linux::*; @@ -15,8 +15,6 @@ use hbb_common::{ use std::{ cell::RefCell, ffi::OsStr, - fs::File, - io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::{Child, Command}, string::String, @@ -612,8 +610,15 @@ pub fn get_env_var(k: &str) -> String { } } +fn is_flatpak() -> bool { + std::path::PathBuf::from("/.flatpak-info").exists() +} + // Headless is enabled, always return true. pub fn is_prelogin() -> bool { + if is_flatpak() { + return false; + } let n = get_active_userid().len(); n < 4 && n > 1 } @@ -730,7 +735,8 @@ pub fn block_input(_v: bool) -> (bool, String) { pub fn is_installed() -> bool { if let Ok(p) = std::env::current_exe() { - p.to_str().unwrap_or_default().starts_with("/usr") || p.to_str().unwrap_or_default().starts_with("/nix/store") + p.to_str().unwrap_or_default().starts_with("/usr") + || p.to_str().unwrap_or_default().starts_with("/nix/store") } else { false } @@ -765,30 +771,18 @@ pub fn quit_gui() { unsafe { gtk_main_quit() }; } +/* pub fn exec_privileged(args: &[&str]) -> ResultType { Ok(Command::new("pkexec").args(args).spawn()?) } +*/ pub fn check_super_user_permission() -> ResultType { - let file = format!( - "/usr/share/{}/files/polkit", - crate::get_app_name().to_lowercase() - ); - let arg; - if Path::new(&file).is_file() { - arg = file.as_str(); - } else { - arg = "echo"; - } - // https://github.com/rustdesk/rustdesk/issues/2756 - if let Ok(status) = Command::new("pkexec").arg(arg).status() { - // https://github.com/rustdesk/rustdesk/issues/5205#issuecomment-1658059657s - Ok(status.code() != Some(126) && status.code() != Some(127)) - } else { - Ok(true) - } + gtk_sudo::run(vec!["echo"])?; + Ok(true) } +/* pub fn elevate(args: Vec<&str>) -> ResultType { let cmd = std::env::current_exe()?; match cmd.to_str() { @@ -823,6 +817,7 @@ pub fn elevate(args: Vec<&str>) -> ResultType { } } } +*/ type GtkSettingsPtr = *mut c_void; type GObjectPtr = *mut c_void; @@ -1323,21 +1318,8 @@ fn has_cmd(cmd: &str) -> bool { .unwrap_or_default() } -pub fn run_cmds_pkexec(cmds: &str) -> bool { - const DONE: &str = "RUN_CMDS_PKEXEC_DONE"; - if let Ok(output) = std::process::Command::new("pkexec") - .arg("sh") - .arg("-c") - .arg(&format!("{cmds} echo {DONE}")) - .output() - { - let out = String::from_utf8_lossy(&output.stdout); - log::debug!("cmds: {cmds}"); - log::debug!("output: {out}"); - out.contains(DONE) - } else { - false - } +pub fn run_cmds_privileged(cmds: &str) -> bool { + crate::platform::gtk_sudo::run(vec![cmds]).is_ok() } pub fn run_me_with(secs: u32) { @@ -1366,17 +1348,20 @@ fn switch_service(stop: bool) -> String { pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { if !has_cmd("systemctl") { + // Failed when installed + flutter run + started by `show_new_window`. return false; } log::info!("Uninstalling service..."); let cp = switch_service(true); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "systemctl disable {app_name}; systemctl stop {app_name}; {cp}" + // systemctl kill rustdesk --tray, execute cp first + if !run_cmds_privileged(&format!( + "{cp} systemctl disable {app_name}; systemctl stop {app_name};" )) { Config::set_option("stop-service".into(), "".into()); return true; } + // systemctl stop will kill child processes, below may not be executed. if show_new_window { run_me_with(2); } @@ -1391,8 +1376,8 @@ pub fn install_service() -> bool { log::info!("Installing service..."); let cp = switch_service(false); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "{cp} systemctl enable {app_name}; systemctl stop {app_name}; systemctl start {app_name};" + if !run_cmds_privileged(&format!( + "{cp} systemctl enable {app_name}; systemctl start {app_name};" )) { Config::set_option("stop-service".into(), "Y".into()); } @@ -1402,9 +1387,9 @@ pub fn install_service() -> bool { fn check_if_stop_service() { if Config::get_option("stop-service".into()) == "Y" { let app_name = crate::get_app_name().to_lowercase(); - allow_err!(run_cmds( + allow_err!(run_cmds(&format!( "systemctl disable {app_name}; systemctl stop {app_name}" - )); + ))); } } @@ -1413,22 +1398,26 @@ pub fn check_autostart_config() -> ResultType<()> { let app_name = crate::get_app_name().to_lowercase(); let path = format!("{home}/.config/autostart"); let file = format!("{path}/{app_name}.desktop"); - std::fs::create_dir_all(&path).ok(); - if !Path::new(&file).exists() { - // write text to the desktop file - let mut file = std::fs::File::create(&file)?; - file.write_all( - format!( - " -[Desktop Entry] -Type=Application -Exec={app_name} --tray -NoDisplay=false - " - ) - .as_bytes(), - )?; - } + // https://github.com/rustdesk/rustdesk/issues/4863 + std::fs::remove_file(&file).ok(); + /* + std::fs::create_dir_all(&path).ok(); + if !Path::new(&file).exists() { + // write text to the desktop file + let mut file = std::fs::File::create(&file)?; + file.write_all( + format!( + " + [Desktop Entry] + Type=Application + Exec={app_name} --tray + NoDisplay=false + " + ) + .as_bytes(), + )?; + } + */ Ok(()) } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index a6887c2791a2..2fb2b46db90a 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -24,9 +24,10 @@ use hbb_common::{ sysinfo::{Pid, Process, ProcessRefreshKind, System}, }; 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::path::{Path, PathBuf}; static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); @@ -60,6 +61,10 @@ pub fn major_version() -> u32 { } pub fn is_process_trusted(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_process_trusted(prompt)) +} + +fn unsafe_is_process_trusted(prompt: bool) -> bool { unsafe { let value = if prompt { YES } else { NO }; let value: id = msg_send![class!(NSNumber), numberWithBool: value]; @@ -79,10 +84,14 @@ pub fn is_can_input_monitoring(prompt: bool) -> bool { } } +pub fn is_can_screen_recording(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_can_screen_recording(prompt)) +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk -pub fn is_can_screen_recording(prompt: bool) -> bool { +fn unsafe_is_can_screen_recording(prompt: bool) -> bool { // we got some report that we show no permission even after set it, so we try to use new api for screen recording check // the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15), // but also some said it has bug on 10.16, so we just use it on 11.0. @@ -297,6 +306,10 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { } pub fn get_focused_display(displays: Vec) -> Option { + autoreleasepool(|| unsafe_get_focused_display(displays)) +} + +fn unsafe_get_focused_display(displays: Vec) -> Option { unsafe { let main_screen: id = msg_send![class!(NSScreen), mainScreen]; let screen: id = msg_send![main_screen, deviceDescription]; @@ -311,6 +324,10 @@ pub fn get_focused_display(displays: Vec) -> Option { } pub fn get_cursor() -> ResultType> { + autoreleasepool(|| unsafe_get_cursor()) +} + +fn unsafe_get_cursor() -> ResultType> { unsafe { let seed = CGSCurrentCursorSeed(); if seed == LATEST_SEED { @@ -375,8 +392,12 @@ fn get_cursor_id() -> ResultType<(id, u64)> { } } -// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c pub fn get_cursor_data(hcursor: u64) -> ResultType { + autoreleasepool(|| unsafe_get_cursor_data(hcursor)) +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { unsafe { let (c, hcursor2) = get_cursor_id()?; if hcursor != hcursor2 { @@ -493,55 +514,7 @@ pub fn lock_screen() { } pub fn start_os_service() { - crate::platform::macos::hide_dock(); 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 + 1 { - log::error!( - "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work", - ); - 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); } @@ -641,7 +614,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; @@ -659,7 +632,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) { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fe66e50dc694..d0ddd09bf700 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,6 +23,9 @@ pub mod linux; #[cfg(target_os = "linux")] pub mod linux_desktop_manager; +#[cfg(target_os = "linux")] +pub mod gtk_sudo; + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{message_proto::CursorData, ResultType}; use std::sync::{Arc, Mutex}; @@ -100,6 +103,7 @@ impl WakeLock { } } +#[cfg(not(target_os = "ios"))] pub fn get_wakelock(_display: bool) -> WakeLock { hbb_common::log::info!("new wakelock, require display on: {_display}"); #[cfg(target_os = "android")] @@ -127,6 +131,12 @@ impl Drop for InstallingService { } } +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +pub fn is_prelogin() -> bool { + false +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/platform/privileges_scripts/daemon.plist b/src/platform/privileges_scripts/daemon.plist index 61efc25eca4c..59f103a31385 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/windows.cc b/src/platform/windows.cc index 7ed76d2e4bee..04095a2d63f0 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -222,6 +222,11 @@ extern "C" return IsWindowsServer(); } + bool is_windows_10_or_greater() + { + return IsWindows10OrGreater(); + } + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid) { HANDLE hProcess = NULL; @@ -549,7 +554,9 @@ extern "C" continue; if (!stricmp(info.pWinStationName, "console")) { - return info.SessionId; + auto id = info.SessionId; + WTSFreeMemory(pInfos); + return id; } if (!strnicmp(info.pWinStationName, rdp, nrdp)) { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 09ff347a295c..6c0136128a1d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,26 +1,25 @@ use super::{CursorData, ResultType}; -use crate::common::PORTABLE_APPNAME_RUNTIME_ENV_KEY; use crate::{ + common::PORTABLE_APPNAME_RUNTIME_ENV_KEY, custom_server::*, ipc, privacy_mode::win_topmost_window::{self, WIN_TOPMOST_INJECTED_PROCESS_EXE}, }; -use hbb_common::libc::{c_int, wchar_t}; use hbb_common::{ allow_err, anyhow::anyhow, bail, config::{self, Config}, + libc::{c_int, wchar_t}, log, message_proto::{DisplayInfo, Resolution, WindowsSession}, sleep, timeout, tokio, }; -use std::process::{Command, Stdio}; use std::{ collections::HashMap, ffi::{CString, OsString}, - fs, io, - io::prelude::*, + fs, + io::{self, prelude::*}, mem, os::windows::process::CommandExt, path::*, @@ -29,14 +28,16 @@ use std::{ time::{Duration, Instant}, }; use wallpaper; -use winapi::um::sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, - libloaderapi::{GetProcAddress, LoadLibraryA}, + libloaderapi::{ + GetProcAddress, LoadLibraryExA, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, + LOAD_LIBRARY_SEARCH_USER_DIRS, + }, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, @@ -44,6 +45,7 @@ use winapi::{ }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, + sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, winbase::*, wingdi::*, winnt::{ @@ -63,13 +65,15 @@ use windows_service::{ }, service_control_handler::{self, ServiceControlHandlerResult}, }; -use winreg::enums::*; -use winreg::RegKey; +use winreg::{enums::*, RegKey}; pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window pub const EXPLORER_EXE: &'static str = "explorer.exe"; 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 fn get_focused_display(displays: Vec) -> Option { unsafe { let hwnd = GetForegroundWindow(); @@ -477,6 +481,7 @@ extern "C" { fn selectInputDesktop() -> BOOL; fn inputDesktopSelected() -> BOOL; fn is_windows_server() -> BOOL; + fn is_windows_10_or_greater() -> BOOL; fn handleMask( out: *mut u8, mask: *const u8, @@ -992,6 +997,32 @@ fn get_valid_subkey() -> String { return get_subkey(&app_name, false); } +// Return install options other than InstallLocation. +pub fn get_install_options() -> String { + let app_name = crate::get_app_name(); + let subkey = format!(".{}", app_name.to_lowercase()); + let mut opts = HashMap::new(); + + let desktop_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_DESKTOPSHORTCUTS); + if let Some(desktop_shortcuts) = desktop_shortcuts { + opts.insert(REG_NAME_INSTALL_DESKTOPSHORTCUTS, desktop_shortcuts); + } + let start_menu_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_STARTMENUSHORTCUTS); + if let Some(start_menu_shortcuts) = start_menu_shortcuts { + opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); + } + serde_json::to_string(&opts).unwrap_or("{}".to_owned()) +} + +// This function return Option, because some registry value may be empty. +fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option { + let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); + if let Ok(tmp) = hkcr.open_subkey(subkey.replace("HKEY_CLASSES_ROOT\\", "")) { + return tmp.get_value(name).ok(); + } + None +} + pub fn get_install_info() -> (String, String, String, String) { get_install_info_with_subkey(get_valid_subkey()) } @@ -1101,7 +1132,11 @@ pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType )) } -fn get_after_install(exe: &str) -> String { +fn get_after_install( + exe: &str, + reg_value_start_menu_shortcuts: Option, + reg_value_desktop_shortcuts: Option, +) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); @@ -1112,9 +1147,24 @@ fn get_after_install(exe: &str) -> String { hcu.delete_subkey_all(format!("Software\\Classes\\{}", exe)) .ok(); + let desktop_shortcuts = reg_value_desktop_shortcuts + .map(|v| { + format!("reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_DESKTOPSHORTCUTS} /t REG_SZ /d \"{v}\"") + }) + .unwrap_or_default(); + let start_menu_shortcuts = reg_value_start_menu_shortcuts + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_STARTMENUSHORTCUTS} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); + format!(" chcp 65001 reg add HKEY_CLASSES_ROOT\\.{ext} /f + {desktop_shortcuts} + {start_menu_shortcuts} 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 @@ -1197,6 +1247,8 @@ oLink.Save .unwrap_or("") .to_owned(); 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 shortcuts = Default::default(); if options.contains("desktopicon") { shortcuts = format!( @@ -1204,6 +1256,7 @@ oLink.Save tmp_path, crate::get_app_name() ); + reg_value_desktop_shortcuts = "1".to_owned(); } if options.contains("startmenu") { shortcuts = format!( @@ -1213,6 +1266,7 @@ copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\" copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" " ); + reg_value_start_menu_shortcuts = "1".to_owned(); } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; @@ -1281,7 +1335,11 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" ", version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, - after_install = get_after_install(&exe), + after_install = get_after_install( + &exe, + Some(reg_value_start_menu_shortcuts), + Some(reg_value_desktop_shortcuts) + ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, @@ -1294,7 +1352,7 @@ 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), true, "after_install") + run_cmds(get_after_install(&exe, None, None), true, "after_install") } pub fn run_before_uninstall() -> ResultType<()> { @@ -1404,15 +1462,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<()> { @@ -1504,10 +1560,68 @@ pub fn is_win_server() -> bool { unsafe { is_windows_server() > 0 } } -pub fn bootstrap() { +#[inline] +pub fn is_win_10_or_greater() -> bool { + unsafe { is_windows_10_or_greater() > 0 } +} + +pub fn bootstrap() -> bool { if let Ok(lic) = get_license_from_exe_name() { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); } + + set_safe_load_dll() +} + +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 +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<()> { @@ -1872,7 +1986,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") @@ -2315,34 +2429,6 @@ fn get_license() -> Option { Some(lic) } -fn get_sid_of_user(username: &str) -> ResultType { - let mut output = Command::new("wmic") - .args(&[ - "useraccount", - "where", - &format!("name='{}'", username), - "get", - "sid", - "/value", - ]) - .creation_flags(CREATE_NO_WINDOW) - .stdout(Stdio::piped()) - .spawn()? - .stdout - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to open stdout"))?; - let mut result = String::new(); - output.read_to_string(&mut result)?; - let sid_start_index = result - .find('=') - .map(|i| i + 1) - .ok_or(anyhow!("bad output format"))?; - if sid_start_index > 0 && sid_start_index < result.len() + 1 { - Ok(result[sid_start_index..].trim().to_string()) - } else { - bail!("bad output format"); - } -} - pub struct WallPaperRemover { old_path: String, } @@ -2378,12 +2464,7 @@ impl WallPaperRemover { // https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/ // https://superuser.com/questions/1218413/write-to-current-users-registry-through-a-different-admin-account let (hkcu, sid) = if is_root() { - let username = get_active_username(); - if username.is_empty() { - bail!("failed to get username"); - } - let sid = get_sid_of_user(&username)?; - log::info!("username: {username}, sid: {sid}"); + let sid = get_current_process_session_id().ok_or(anyhow!("failed to get sid"))?; (RegKey::predef(HKEY_USERS), format!("{}\\", sid)) } else { (RegKey::predef(HKEY_CURRENT_USER), "".to_string()) @@ -2504,7 +2585,11 @@ 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 = LoadLibraryExA( + CString::new("ntdll.dll")?.as_ptr(), + std::ptr::null_mut(), + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); if !h_module.is_null() { let f_nt_terminate_process: NtTerminateProcess = std::mem::transmute(GetProcAddress( h_module, @@ -2540,3 +2625,106 @@ pub fn try_set_window_foreground(window: HWND) { } } } + +pub mod reg_display_settings { + use hbb_common::ResultType; + use serde_derive::{Deserialize, Serialize}; + use std::collections::HashMap; + use winreg::{enums::*, RegValue}; + const REG_GRAPHICS_DRIVERS_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers"; + const REG_CONNECTIVITY_PATH: &str = "Connectivity"; + + #[derive(Serialize, Deserialize, Debug)] + pub struct RegRecovery { + path: String, + key: String, + old: (Vec, isize), + new: (Vec, isize), + } + + pub fn read_reg_connectivity() -> ResultType>> { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + let reg_connectivity = hklm.open_subkey_with_flags( + format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), + KEY_READ, + )?; + + let mut map_connectivity = HashMap::new(); + for key in reg_connectivity.enum_keys() { + let key = key?; + let mut map_item = HashMap::new(); + let reg_item = reg_connectivity.open_subkey_with_flags(&key, KEY_READ)?; + for value in reg_item.enum_values() { + let (name, value) = value?; + map_item.insert(name, value); + } + map_connectivity.insert(key, map_item); + } + Ok(map_connectivity) + } + + pub fn diff_recent_connectivity( + map1: HashMap>, + map2: HashMap>, + ) -> Option { + for (subkey, map_item2) in map2 { + if let Some(map_item1) = map1.get(&subkey) { + let key = "Recent"; + if let Some(value1) = map_item1.get(key) { + if let Some(value2) = map_item2.get(key) { + if value1 != value2 { + return Some(RegRecovery { + path: format!( + "{}\\{}\\{}", + REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH, subkey + ), + key: key.to_owned(), + old: (value1.bytes.clone(), value1.vtype.clone() as isize), + new: (value2.bytes.clone(), value2.vtype.clone() as isize), + }); + } + } + } + } + } + None + } + + pub fn restore_reg_connectivity(reg_recovery: RegRecovery) -> 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(()); + } + let reg_value = RegValue { + bytes: reg_recovery.old.0, + vtype: isize_to_reg_type(reg_recovery.old.1), + }; + reg_item.set_raw_value(®_recovery.key, ®_value)?; + Ok(()) + } + + #[inline] + fn isize_to_reg_type(i: isize) -> RegType { + match i { + 0 => RegType::REG_NONE, + 1 => RegType::REG_SZ, + 2 => RegType::REG_EXPAND_SZ, + 3 => RegType::REG_BINARY, + 4 => RegType::REG_DWORD, + 5 => RegType::REG_DWORD_BIG_ENDIAN, + 6 => RegType::REG_LINK, + 7 => RegType::REG_MULTI_SZ, + 8 => RegType::REG_RESOURCE_LIST, + 9 => RegType::REG_FULL_RESOURCE_DESCRIPTOR, + 10 => RegType::REG_RESOURCE_REQUIREMENTS_LIST, + 11 => RegType::REG_QWORD, + _ => RegType::REG_NONE, + } + } +} diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs index 74a7f736f241..f59e4c9ff78c 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 b40ee4116760..8164e19bd819 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}, }; @@ -299,7 +299,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/privacy_mode.rs b/src/privacy_mode.rs index 7116095f1e1b..a02b8bc93eba 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -38,14 +38,9 @@ pub const TURN_OFF_OTHER_ID: &'static str = "Failed to turn off privacy mode that belongs to someone else."; pub const NO_PHYSICAL_DISPLAYS: &'static str = "no_need_privacy_mode_no_physical_displays_tip"; -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = win_mag::PRIVACY_MODE_IMPL; -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE: &str = - win_exclude_from_capture::PRIVACY_MODE_IMPL; - -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY: &str = win_virtual_display::PRIVACY_MODE_IMPL; +pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = "privacy_mode_impl_mag"; +pub const PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE: &str = "privacy_mode_impl_exclude_from_capture"; +pub const PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY: &str = "privacy_mode_impl_virtual_display"; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] @@ -187,6 +182,7 @@ fn get_supported_impl(impl_key: &str) -> String { if supported_impls.iter().any(|(k, _)| k == &impl_key) { return impl_key.to_owned(); }; + // TODO: Is it a good idea to use fallback here? Because user do not know the fallback. // fallback let mut cur_impl = get_option("privacy-mode-impl-key".to_owned()); if !get_supported_privacy_mode_impl() @@ -223,6 +219,8 @@ async fn turn_on_privacy_async(impl_key: String, conn_id: i32) -> Option match res { Ok(res) => res, diff --git a/src/privacy_mode/win_exclude_from_capture.rs b/src/privacy_mode/win_exclude_from_capture.rs index 63164f838fb1..7d680011f0af 100644 --- a/src/privacy_mode/win_exclude_from_capture.rs +++ b/src/privacy_mode/win_exclude_from_capture.rs @@ -2,7 +2,7 @@ use hbb_common::platform::windows::is_windows_version_or_greater; pub use super::win_topmost_window::PrivacyModeImpl; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_exclude_from_capture"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE; pub(super) fn is_supported() -> bool { // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity diff --git a/src/privacy_mode/win_mag.rs b/src/privacy_mode/win_mag.rs index a93dce350286..6235c24838e2 100644 --- a/src/privacy_mode/win_mag.rs +++ b/src/privacy_mode/win_mag.rs @@ -4,7 +4,7 @@ use std::time::Instant; pub use super::win_topmost_window::PrivacyModeImpl; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_mag"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_MAG; pub fn create_capturer( privacy_mode_id: i32, diff --git a/src/privacy_mode/win_topmost_window.rs b/src/privacy_mode/win_topmost_window.rs index fdfcfcba6e17..a7f80a02d726 100644 --- a/src/privacy_mode/win_topmost_window.rs +++ b/src/privacy_mode/win_topmost_window.rs @@ -75,7 +75,7 @@ impl PrivacyMode for PrivacyModeImpl { fn is_async_privacy_mode(&self) -> bool { false } - + fn init(&self) -> ResultType<()> { Ok(()) } diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 0bf4b721f480..d235575fdacf 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -1,5 +1,5 @@ use super::{PrivacyMode, PrivacyModeState, INVALID_PRIVACY_MODE_CONN_ID, NO_PHYSICAL_DISPLAYS}; -use crate::virtual_display_manager; +use crate::{platform::windows::reg_display_settings, virtual_display_manager}; use hbb_common::{allow_err, bail, config::Config, log, ResultType}; use std::{ io::Error, @@ -21,20 +21,20 @@ use winapi::{ winuser::{ ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsExW, EnumDisplaySettingsW, CDS_NORESET, CDS_RESET, CDS_SET_PRIMARY, CDS_UPDATEREGISTRY, - DISP_CHANGE_SUCCESSFUL, EDD_GET_DEVICE_INTERFACE_NAME, ENUM_CURRENT_SETTINGS, - ENUM_REGISTRY_SETTINGS, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, EDD_GET_DEVICE_INTERFACE_NAME, + ENUM_CURRENT_SETTINGS, ENUM_REGISTRY_SETTINGS, }, }, }; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_virtual_display"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY; const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; struct Display { dm: DEVMODEW, name: [WCHAR; 32], - _primary: bool, + primary: bool, } pub struct PrivacyModeImpl { @@ -135,7 +135,7 @@ impl PrivacyModeImpl { let display = Display { dm, name: dd.DeviceName, - _primary: primary, + primary, }; let ds = virtual_display_manager::get_cur_device_string(); @@ -150,12 +150,30 @@ impl PrivacyModeImpl { } fn restore_plug_out_monitor(&mut self) { - let _ = virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added); + let _ = virtual_display_manager::plug_out_monitor_indices( + &self.virtual_displays_added, + true, + false, + ); self.virtual_displays_added.clear(); } - fn set_primary_display(&mut self) -> ResultType<()> { + #[inline] + fn change_display_settings_ex_err_msg(rc: i32) -> String { + if rc != DISP_CHANGE_FAILED { + format!("ret: {}", rc) + } else { + format!( + "ret: {}, last error: {:?}", + rc, + std::io::Error::last_os_error() + ) + } + } + + fn set_primary_display(&mut self) -> ResultType { let display = &self.virtual_displays[0]; + let display_name = std::string::String::from_utf16(&display.name)?; #[allow(invalid_value)] let mut new_primary_dm: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; @@ -220,20 +238,29 @@ impl PrivacyModeImpl { flags, NULL, ); - if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); log::error!( - "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, ret: {}", + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", std::string::String::from_utf16(&dd.DeviceName), flags, - rc + &err ); - bail!("Failed ChangeDisplaySettingsEx, ret: {}", rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } + + // If we want to set dpi, the following references may be helpful. + // And setting dpi should be called after changing the display settings. + // https://stackoverflow.com/questions/35233182/how-can-i-change-windows-10-display-scaling-programmatically-using-c-sharp + // https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.cpp + // + // But the official API does not provide a way to get/set dpi. + // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ne-wingdi-displayconfig_device_info_type + // https://github.com/lihas/windows-DPI-scaling-sample/blob/738ac18b7a7ce2d8fdc157eb825de9cb5eee0448/DPIHelper/DpiHelper.h#L37 } } - Ok(()) + Ok(display_name) } fn disable_physical_displays(&self) -> ResultType<()> { @@ -253,13 +280,14 @@ impl PrivacyModeImpl { NULL as _, ); if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); log::error!( - "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, ret: {}", + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", std::string::String::from_utf16(&display.name), flags, - rc + &err ); - bail!("Failed ChangeDisplaySettingsEx, ret: {}", rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } } } @@ -286,7 +314,7 @@ impl PrivacyModeImpl { // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { - virtual_display_manager::plug_out_monitor_indices(&displays)?; + virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; bail!(NO_PHYSICAL_DISPLAYS); } @@ -316,9 +344,10 @@ impl PrivacyModeImpl { // } // } - let ret = ChangeDisplaySettingsExW(NULL as _, NULL as _, NULL as _, flags, NULL as _); - if ret != DISP_CHANGE_SUCCESSFUL { - bail!("Failed ChangeDisplaySettingsEx, ret: {}", ret); + let rc = ChangeDisplaySettingsExW(NULL as _, NULL as _, NULL as _, flags, NULL as _); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } // if !desk_current.is_null() { @@ -330,6 +359,35 @@ impl PrivacyModeImpl { } Ok(()) } + + fn restore(&mut self) { + 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(); + } + + fn restore_displays(displays: &[Display]) { + for display in displays { + unsafe { + let mut dm = display.dm.clone(); + let flags = if display.primary { + CDS_NORESET | CDS_UPDATEREGISTRY | CDS_SET_PRIMARY + } else { + CDS_NORESET | CDS_UPDATEREGISTRY + }; + ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut dm, + std::ptr::null_mut(), + flags, + std::ptr::null_mut(), + ); + } + } + } } impl PrivacyMode for PrivacyModeImpl { @@ -372,9 +430,11 @@ impl PrivacyMode for PrivacyModeImpl { } let reg_connectivity_1 = reg_display_settings::read_reg_connectivity()?; - guard.set_primary_display()?; + let primary_display_name = guard.set_primary_display()?; 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)); let reg_connectivity_2 = reg_display_settings::read_reg_connectivity()?; if let Some(reg_recovery) = @@ -404,7 +464,8 @@ impl PrivacyMode for PrivacyModeImpl { ) -> ResultType<()> { self.check_off_conn_id(conn_id)?; super::win_input::unhook()?; - self.restore_plug_out_monitor(); + let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); + self.restore(); restore_reg_connectivity(false); if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { @@ -452,7 +513,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { return; } if plug_out_monitors { - let _ = virtual_display_manager::plug_out_monitor(-1); + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); } if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) @@ -463,107 +524,3 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { } reset_config_reg_connectivity(); } - -mod reg_display_settings { - use hbb_common::ResultType; - use serde_derive::{Deserialize, Serialize}; - use std::collections::HashMap; - use winreg::{enums::*, RegValue}; - const REG_GRAPHICS_DRIVERS_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers"; - const REG_CONNECTIVITY_PATH: &str = "Connectivity"; - - #[derive(Serialize, Deserialize, Debug)] - pub(super) struct RegRecovery { - path: String, - key: String, - old: (Vec, isize), - new: (Vec, isize), - } - - pub(super) fn read_reg_connectivity() -> ResultType>> - { - let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); - let reg_connectivity = hklm.open_subkey_with_flags( - format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), - KEY_READ, - )?; - - let mut map_connectivity = HashMap::new(); - for key in reg_connectivity.enum_keys() { - let key = key?; - let mut map_item = HashMap::new(); - let reg_item = reg_connectivity.open_subkey_with_flags(&key, KEY_READ)?; - for value in reg_item.enum_values() { - let (name, value) = value?; - map_item.insert(name, value); - } - map_connectivity.insert(key, map_item); - } - Ok(map_connectivity) - } - - pub(super) fn diff_recent_connectivity( - map1: HashMap>, - map2: HashMap>, - ) -> Option { - for (subkey, map_item2) in map2 { - if let Some(map_item1) = map1.get(&subkey) { - let key = "Recent"; - if let Some(value1) = map_item1.get(key) { - if let Some(value2) = map_item2.get(key) { - if value1 != value2 { - return Some(RegRecovery { - path: format!( - "{}\\{}\\{}", - REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH, subkey - ), - key: key.to_owned(), - old: (value1.bytes.clone(), value1.vtype.clone() as isize), - new: (value2.bytes.clone(), value2.vtype.clone() as isize), - }); - } - } - } - } - } - None - } - - pub(super) fn restore_reg_connectivity(reg_recovery: RegRecovery) -> 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(()); - } - let reg_value = RegValue { - bytes: reg_recovery.old.0, - vtype: isize_to_reg_type(reg_recovery.old.1), - }; - reg_item.set_raw_value(®_recovery.key, ®_value)?; - Ok(()) - } - - #[inline] - fn isize_to_reg_type(i: isize) -> RegType { - match i { - 0 => RegType::REG_NONE, - 1 => RegType::REG_SZ, - 2 => RegType::REG_EXPAND_SZ, - 3 => RegType::REG_BINARY, - 4 => RegType::REG_DWORD, - 5 => RegType::REG_DWORD_BIG_ENDIAN, - 6 => RegType::REG_LINK, - 7 => RegType::REG_MULTI_SZ, - 8 => RegType::REG_RESOURCE_LIST, - 9 => RegType::REG_FULL_RESOURCE_DESCRIPTOR, - 10 => RegType::REG_RESOURCE_REQUIREMENTS_LIST, - 11 => RegType::REG_QWORD, - _ => RegType::REG_NONE, - } - } -} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 65bb6a2b3ed5..69fc886cac96 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -12,10 +12,7 @@ use uuid::Uuid; use hbb_common::{ allow_err, anyhow::{self, bail}, - config::{ - self, keys::*, option2bool, Config, CONNECT_TIMEOUT, READ_TIMEOUT, REG_INTERVAL, - RENDEZVOUS_PORT, - }, + config::{self, keys::*, option2bool, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, futures::future::join_all, log, protobuf::Message as _, @@ -32,7 +29,7 @@ use hbb_common::{ use crate::{ check_port, server::{check_zombie, new as new_server, ServerPtr}, - ui_interface::get_buildin_option, + ui_interface::get_builtin_option, }; type Message = RendezvousMessage; @@ -79,8 +76,11 @@ impl RendezvousMediator { tokio::spawn(async move { direct_server(server_cloned).await; }); + #[cfg(target_os = "android")] + let start_lan_listening = true; #[cfg(not(any(target_os = "android", target_os = "ios")))] - if crate::platform::is_installed() { + let start_lan_listening = crate::platform::is_installed(); + if start_lan_listening { std::thread::spawn(move || { allow_err!(super::lan::start_listening()); }); @@ -90,6 +90,7 @@ impl RendezvousMediator { if crate::is_server() { crate::platform::linux_desktop_manager::start_xdesktop(); } + scrap::codec::test_av1(); loop { let conn_start_time = Instant::now(); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); @@ -391,7 +392,7 @@ impl RendezvousMediator { }; if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) || is_http_proxy - || get_buildin_option(config::keys::OPTION_DISABLE_UDP) == "Y" + || get_builtin_option(config::keys::OPTION_DISABLE_UDP) == "Y" { Self::start_tcp(server, host).await } else { @@ -703,123 +704,6 @@ async fn direct_server(server: ServerPtr) { } } -pub async fn query_online_states, Vec)>(ids: Vec, f: F) { - let test = false; - if test { - sleep(1.5).await; - let mut onlines = ids; - let offlines = onlines.drain((onlines.len() / 2)..).collect(); - f(onlines, offlines) - } else { - let query_begin = Instant::now(); - let query_timeout = std::time::Duration::from_millis(3_000); - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - break; - } - match query_online_states_(&ids, query_timeout).await { - Ok((onlines, offlines)) => { - f(onlines, offlines); - break; - } - Err(e) => { - log::debug!("{}", &e); - } - } - - if query_begin.elapsed() > query_timeout { - log::debug!( - "query onlines timeout {:?} ({:?})", - query_begin.elapsed(), - query_timeout - ); - break; - } - - sleep(1.5).await; - } - } -} - -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(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", rendezvous_server); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", rendezvous_server); - } - let online_server = format!("{}:{}", tmp[0], port - 1); - connect_tcp(online_server, CONNECT_TIMEOUT).await -} - -async fn query_online_states_( - ids: &Vec, - timeout: std::time::Duration, -) -> ResultType<(Vec, Vec)> { - let query_begin = Instant::now(); - - let mut msg_out = RendezvousMessage::new(); - msg_out.set_online_request(OnlineRequest { - id: Config::get_id(), - peers: ids.clone(), - ..Default::default() - }); - - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - // No need to care about onlines - return Ok((Vec::new(), Vec::new())); - } - - let mut socket = match create_online_stream().await { - Ok(s) => s, - Err(e) => { - log::debug!("Failed to create peers online stream, {e}"); - return Ok((vec![], ids.clone())); - } - }; - if let Err(e) = socket.send(&msg_out).await { - log::debug!("Failed to send peers online states query, {e}"); - return Ok((vec![], ids.clone())); - } - if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await { - match msg_in.union { - Some(rendezvous_message::Union::OnlineResponse(online_response)) => { - let states = online_response.states; - let mut onlines = Vec::new(); - let mut offlines = Vec::new(); - for i in 0..ids.len() { - // bytes index from left to right - let bit_value = 0x01 << (7 - i % 8); - if (states[i / 8] & bit_value) == bit_value { - onlines.push(ids[i].clone()); - } else { - offlines.push(ids[i].clone()); - } - } - return Ok((onlines, offlines)); - } - _ => { - // ignore - } - } - } else { - // TODO: Make sure socket closed? - bail!("Online stream receives None"); - } - - if query_begin.elapsed() > timeout { - bail!("Try query onlines timeout {:?}", &timeout); - } - - sleep(300.0).await; - } -} - enum Sink<'a> { Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), Stream(&'a mut FramedStream), @@ -833,24 +717,3 @@ impl Sink<'_> { } } } - -#[cfg(test)] -mod tests { - use hbb_common::tokio; - - #[tokio::test] - async fn test_query_onlines() { - super::query_online_states( - vec![ - "152183996".to_owned(), - "165782066".to_owned(), - "155323351".to_owned(), - "460952777".to_owned(), - ], - |onlines: Vec, offlines: Vec| { - println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); - }, - ) - .await; - } -} diff --git a/src/server.rs b/src/server.rs index 6505ad1c2d34..ba1682f3d0f5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -32,8 +32,10 @@ use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { -if #[cfg(not(any(target_os = "android", target_os = "ios")))] { +if #[cfg(not(target_os = "ios"))] { mod clipboard_service; +#[cfg(target_os = "android")] +pub use clipboard_service::is_clipboard_service_ok; #[cfg(target_os = "linux")] pub(crate) mod wayland; #[cfg(target_os = "linux")] @@ -42,17 +44,20 @@ pub mod uinput; pub mod rdp_input; #[cfg(target_os = "linux")] pub mod dbus; +#[cfg(not(target_os = "android"))] pub mod input_service; } else { mod clipboard_service { pub const NAME: &'static str = ""; } -pub mod input_service { -pub const NAME_CURSOR: &'static str = ""; -pub const NAME_POS: &'static str = ""; -pub const NAME_WINDOW_FOCUS: &'static str = ""; } } + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod input_service { + pub const NAME_CURSOR: &'static str = ""; + pub const NAME_POS: &'static str = ""; + pub const NAME_WINDOW_FOCUS: &'static str = ""; } mod connection; @@ -99,13 +104,21 @@ pub fn new() -> ServerPtr { }; server.add_service(Box::new(audio_service::new())); #[cfg(not(target_os = "ios"))] - server.add_service(Box::new(display_service::new())); - #[cfg(not(any(target_os = "android", target_os = "ios")))] { + server.add_service(Box::new(display_service::new())); server.add_service(Box::new(clipboard_service::new())); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); + #[cfg(target_os = "linux")] + if scrap::is_x11() { + // wayland does not support multiple displays currently + server.add_service(Box::new(input_service::new_window_focus())); + } + #[cfg(not(target_os = "linux"))] server.add_service(Box::new(input_service::new_window_focus())); } } @@ -456,16 +469,21 @@ pub async fn start_server(_is_server: bool) { /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. +/// * `no_server` - If `is_server` is false, whether to start a server if not found. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] -pub async fn start_server(is_server: bool) { - #[cfg(target_os = "linux")] - { - log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); - log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); - } - #[cfg(windows)] - hbb_common::platform::windows::start_cpu_performance_monitor(); +pub async fn start_server(is_server: bool, no_server: bool) { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + #[cfg(windows)] + hbb_common::platform::windows::start_cpu_performance_monitor(); + }); if is_server { crate::common::set_server_running(true); @@ -481,7 +499,7 @@ pub async fn start_server(is_server: bool) { }); input_service::fix_key_down_timeout_loop(); #[cfg(target_os = "linux")] - if crate::platform::current_is_wayland() { + if input_service::wayland_use_uinput() { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); } #[cfg(any(target_os = "macos", target_os = "linux"))] @@ -489,7 +507,6 @@ pub async fn start_server(is_server: bool) { #[cfg(target_os = "windows")] crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] scrap::hwcodec::start_check_process(); crate::RendezvousMediator::start_all().await; } else { @@ -516,8 +533,14 @@ pub async fn start_server(is_server: bool) { crate::ipc::client_get_hwcodec_config_thread(0); } Err(err) => { - log::info!("server not started (will try to start): {}", err); - std::thread::spawn(|| start_server(true)); + log::info!("server not started: {err:?}, no_server: {no_server}"); + if no_server { + hbb_common::sleep(1.0).await; + std::thread::spawn(|| start_server(false, true)); + } else { + log::info!("try start server"); + std::thread::spawn(|| start_server(true, false)); + } } } } diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index f227bd232880..d1bb2d87842d 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -78,6 +78,19 @@ pub fn restart() { #[cfg(any(target_os = "linux", target_os = "android"))] mod pa_impl { use super::*; + + // SAFETY: constrains of hbb_common::mem::aligned_u8_vec must be held + unsafe fn align_to_32(data: Vec) -> Vec { + if (data.as_ptr() as usize & 3) == 0 { + return data; + } + + let mut buf = vec![]; + buf = unsafe { hbb_common::mem::aligned_u8_vec(data.len(), 4) }; + buf.extend_from_slice(data.as_ref()); + buf + } + #[tokio::main(flavor = "current_thread")] pub async fn run(sp: EmptyExtraFieldService) -> ResultType<()> { hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc @@ -106,23 +119,29 @@ mod pa_impl { sps.send(create_format_msg(crate::platform::PA_SAMPLE_RATE, 2)); Ok(()) })?; + #[cfg(target_os = "linux")] if let Ok(data) = stream.next_raw().await { if data.len() == 0 { send_f32(&zero_audio_frame, &mut encoder, &sp); continue; } + if data.len() != AUDIO_DATA_SIZE_U8 { continue; } + + let data = unsafe { align_to_32(data.into()) }; let data = unsafe { std::slice::from_raw_parts::(data.as_ptr() as _, data.len() / 4) }; send_f32(data, &mut encoder, &sp); } + #[cfg(target_os = "android")] if scrap::android::ffi::get_audio_raw(&mut android_data, &mut vec![]).is_some() { let data = unsafe { + android_data = align_to_32(android_data); std::slice::from_raw_parts::( android_data.as_ptr() as _, android_data.len() / 4, @@ -137,6 +156,14 @@ mod pa_impl { } } +#[inline] +#[cfg(feature = "screencapturekit")] +pub fn is_screen_capture_kit_available() -> bool { + cpal::available_hosts() + .iter() + .any(|host| *host == cpal::HostId::ScreenCaptureKit) +} + #[cfg(not(any(target_os = "linux", target_os = "android")))] mod cpal_impl { use self::service::{Reset, ServiceSwap}; @@ -151,6 +178,11 @@ mod cpal_impl { static ref INPUT_BUFFER: Arc>> = Default::default(); } + #[cfg(feature = "screencapturekit")] + lazy_static::lazy_static! { + static ref HOST_SCREEN_CAPTURE_KIT: Result = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); + } + #[derive(Default)] pub struct State { stream: Option<(Box, Arc)>, @@ -227,6 +259,27 @@ mod cpal_impl { send_f32(&data, encoder, sp); } + #[cfg(feature = "screencapturekit")] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + if !is_screen_capture_kit_available() { + return get_audio_input(""); + } + let device = HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .default_input_device() + .with_context(|| "Failed to get default input device for loopback")?; + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get input output format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + #[cfg(windows)] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); @@ -248,7 +301,7 @@ mod cpal_impl { Ok((device, format)) } - #[cfg(not(windows))] + #[cfg(not(any(windows, feature = "screencapturekit")))] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); get_audio_input(&audio_input) @@ -256,7 +309,20 @@ mod cpal_impl { fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { let mut device = None; - if !audio_input.is_empty() { + #[cfg(feature = "screencapturekit")] + if !audio_input.is_empty() && is_screen_capture_kit_available() { + for d in HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() && !audio_input.is_empty() { for d in HOST .devices() .with_context(|| "Failed to get audio devices")? diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index eeeea4999c63..8ae482500550 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,49 +1,246 @@ use super::*; -pub use crate::clipboard::{ - check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, - CONTENT, +#[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(not(target_os = "android"))] +use clipboard_master::{CallbackResult, ClipboardHandler}; +#[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}, + time::Duration, }; +#[cfg(windows)] +use tokio::runtime::Runtime; -#[derive(Default)] -struct State { +#[cfg(target_os = "android")] +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")] + rt: Option, +} + +#[cfg(target_os = "android")] +pub fn is_clipboard_service_ok() -> bool { + CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) } -impl super::service::Reset for State { - fn reset(&mut self) { - *CONTENT.lock().unwrap() = Default::default(); - self.ctx = None; +pub fn new() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +#[cfg(not(target_os = "android"))] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + let (tx_cb_result, rx_cb_result) = channel(); + let handler = Handler { + sp: sp.clone(), + ctx: Some(ClipboardContext::new()?), + tx_cb_result, + #[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::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + bail!("Clipboard listener stopped with error: {}", err); + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} + } } + shutdown.signal(); + h.join().ok(); - fn init(&mut self) { - let ctx = match ClipboardContext::new(true) { - Ok(ctx) => Some(ctx), - Err(err) => { - log::error!("Failed to start {}: {}", NAME, err); - None + 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); } - }; - self.ctx = ctx; + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + self.tx_cb_result + .send(CallbackResult::StopWithError(error)) + .ok(); + CallbackResult::Next } } -pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); - GenericService::repeat::(&svc.clone(), INTERVAL, run); - svc.sp +#[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() { + match self.read_clipboard_from_cm_ipc() { + Err(e) => { + log::error!("Failed to read clipboard from cm: {}", e); + } + Ok(data) => { + // Skip sending empty clipboard data. + // Maybe there's something wrong reading the clipboard data in cm, but no error msg is returned. + // The clipboard data should not be empty, the last line will try again to get the clipboard data. + if !data.is_empty() { + let mut msg = Message::new(); + let multi_clipboards = MultiClipboards { + clipboards: data + .into_iter() + .map(|c| Clipboard { + compress: c.compress, + content: c.content, + width: c.width, + height: c.height, + format: ClipboardFormat::from_i32(c.format) + .unwrap_or(ClipboardFormat::Text) + .into(), + special_name: c.special_name, + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } + } + } + } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) + } + + // Read clipboard data from cm using ipc. + // + // We cannot use `#[tokio::main(flavor = "current_thread")]` here, + // because the auto-managed tokio runtime (async context) will be dropped after the call. + // The next call will create a new runtime, which will cause the previous stream to be unusable. + // So we need to manage the tokio runtime manually. + #[cfg(windows)] + fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + if self.rt.is_none() { + self.rt = Some(Runtime::new()?); + } + let Some(rt) = &self.rt else { + // unreachable! + bail!("failed to get tokio runtime"); + }; + let mut is_sent = false; + if let Some(stream) = &mut self.stream { + // If previous stream is still alive, reuse it. + // If the previous stream is dead, `is_sent` will trigger reconnect. + is_sent = match rt.block_on(stream.send(&Data::ClipboardNonFile(None))) { + Ok(_) => true, + Err(e) => { + log::debug!("Failed to send to cm: {}", e); + false + } + }; + } + if !is_sent { + let mut stream = rt.block_on(crate::ipc::connect(100, "_cm"))?; + rt.block_on(stream.send(&Data::ClipboardNonFile(None)))?; + self.stream = Some(stream); + } + + if let Some(stream) = &mut self.stream { + loop { + match rt.block_on(stream.next_timeout(800))? { + Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { + if !err.is_empty() { + bail!("{}", err); + } else { + if contents.iter().any(|c| c.next_raw) { + // Wrap the future with a `Timeout` in an async block to avoid panic. + // We cannot use `rt.block_on(timeout(1000, stream.next_raw()))` here, because it causes panic: + // thread '' panicked at D:\Projects\rust\rustdesk\libs\hbb_common\src\lib.rs:98:5: + // there is no reactor running, must be called from the context of a Tokio 1.x runtime + // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + match rt.block_on(async { timeout(1000, stream.next_raw()).await }) + { + Ok(Ok(mut data)) => { + for c in &mut contents { + if c.next_raw { + // No need to check the length because sum(content_len) == data.len(). + c.content = data.split_to(c.content_len).into(); + } + } + } + Ok(Err(e)) => { + // reset by peer + self.stream = None; + bail!("failed to get raw clipboard data: {}", e); + } + Err(e) => { + // Reconnect to avoid the next raw data remaining in the buffer. + self.stream = None; + log::debug!("Failed to get raw clipboard data: {}", e); + } + } + } + return Ok(contents); + } + } + Some(Data::ClipboardFile(ClipboardFile::MonitorReady)) => { + // ClipboardFile::MonitorReady is the first message sent by cm. + } + _ => { + bail!("failed to get clipboard data from cm"); + } + } + } + } + // unreachable! + bail!("failed to get clipboard data from cm"); + } } -fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { - if let Some(msg) = check_clipboard(&mut state.ctx, None) { - sp.send(msg); +#[cfg(target_os = "android")] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + CLIPBOARD_SERVICE_OK.store(sp.ok(), Ordering::SeqCst); + while sp.ok() { + if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { + sp.send(msg); + } + std::thread::sleep(Duration::from_millis(INTERVAL)); } - sp.snapshot(|sps| { - let data = CONTENT.lock().unwrap().clone(); - if !data.is_empty() { - let msg_out = data.create_msg(); - sps.send_shared(Arc::new(msg_out)); - } - Ok(()) - })?; + CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 4a383fe356a8..326d128777f0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,6 +1,6 @@ use super::{input_service::*, *}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::update_clipboard; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(target_os = "android")] @@ -27,12 +27,13 @@ use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ - config::{self, Config}, + config::{self, keys, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection}, 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, @@ -45,7 +46,6 @@ use hbb_common::{ use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; 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::{ @@ -64,9 +64,9 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); - static ref SESSIONS: Arc::>> = 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())); } @@ -140,10 +140,15 @@ enum MessageInput { BlockOffPlugin(String), } -#[derive(Clone, Debug)] -struct Session { +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct SessionKey { + peer_id: String, name: String, session_id: u64, +} + +#[derive(Clone, Debug)] +struct Session { last_recv_time: Arc>, random_password: String, tfa: bool, @@ -210,7 +215,7 @@ pub struct Connection { server_audit_conn: String, server_audit_file: String, lr: LoginRequest, - last_recv_time: Arc>, + session_last_recv_time: Option>>, chat_unanswered: bool, file_transferred: bool, #[cfg(windows)] @@ -223,7 +228,6 @@ pub struct Connection { #[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)>, @@ -335,7 +339,7 @@ impl Connection { clipboard: Connection::permission("enable-clipboard"), audio: Connection::permission("enable-audio"), // to-do: make sure is the option correct here - file: Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER), + file: Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER), restart: Connection::permission("enable-remote-restart"), recording: Connection::permission("enable-record-session"), block_input: Connection::permission("enable-block-input"), @@ -357,7 +361,7 @@ impl Connection { server_audit_conn: "".to_owned(), server_audit_file: "".to_owned(), lr: Default::default(), - last_recv_time: Arc::new(Mutex::new(Instant::now())), + session_last_recv_time: None, chat_unanswered: false, file_transferred: false, #[cfg(windows)] @@ -371,7 +375,6 @@ impl Connection { #[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, @@ -458,11 +461,6 @@ impl Connection { conn.on_close("connection manager", true).await; break; } - #[cfg(target_os = "android")] - ipc::Data::InputControl(v) => { - conn.keyboard = v; - conn.send_permission(Permission::Keyboard, v).await; - } ipc::Data::CmErr(e) => { if e != "expected" { // cm closed before connection @@ -487,6 +485,9 @@ impl Connection { conn.keyboard = enabled; conn.send_permission(Permission::Keyboard, enabled).await; if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.can_sub_clipboard_service()); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -497,15 +498,17 @@ impl Connection { if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, - conn.inner.clone(), conn.clipboard_enabled() && conn.peer_keyboard_enabled()); + conn.inner.clone(), conn.can_sub_clipboard_service()); } } else if &name == "audio" { conn.audio = enabled; conn.send_permission(Permission::Audio, enabled).await; - if let Some(s) = conn.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - conn.inner.clone(), conn.audio_enabled()); + if conn.authorized { + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } } } else if &name == "file" { conn.file = enabled; @@ -586,7 +589,7 @@ impl Connection { }, Ok(bytes) => { last_recv_time = Instant::now(); - *conn.last_recv_time.lock().unwrap() = Instant::now(); + conn.session_last_recv_time.as_mut().map(|t| *t.lock().unwrap() = Instant::now()); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if !conn.on_message(msg_in).await { break; @@ -682,8 +685,19 @@ impl Connection { msg = Arc::new(new_msg); } } + Some(message::Union::MultiClipboards(_multi_clipboards)) => { + #[cfg(not(target_os = "ios"))] + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { + if let Err(err) = conn.stream.send(&msg_out).await { + conn.on_close(&err.to_string(), false).await; + break; + } + continue; + } + } _ => {} } + let msg: &Message = &msg; if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; @@ -720,7 +734,11 @@ 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() { + 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()); + } + } } } } @@ -742,6 +760,7 @@ impl Connection { } if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false).await; + raii::AuthedConnID::check_remove_session(conn.inner.id(), conn.session_key()); } conn.post_conn_audit(json!({ @@ -774,12 +793,13 @@ impl Connection { handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { - // todo: press and down have similar meanings. - if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { + // Set the press state to false, use `down` only in `handle_key()`. + msg.press = false; + if press { msg.down = true; } handle_key(&msg); - if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { + if press { msg.down = false; handle_key(&msg); } @@ -1118,7 +1138,13 @@ impl Connection { self.authed_conn_id = Some(self::raii::AuthedConnID::new( self.inner.id(), auth_conn_type, + self.session_key(), )); + self.session_last_recv_time = SESSIONS + .lock() + .unwrap() + .get(&self.session_key()) + .map(|s| s.last_recv_time.clone()); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), ); @@ -1266,29 +1292,9 @@ impl Connection { self.send(msg_out).await; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - #[cfg(not(windows))] - let displays = display_service::try_get_displays(); - #[cfg(windows)] - let displays = display_service::try_get_displays_add_amyuni_headless(); - pi.resolutions = Some(SupportedResolutions { - resolutions: displays - .map(|displays| { - displays - .get(self.display_idx) - .map(|d| crate::platform::resolutions(&d.name())) - .unwrap_or(vec![]) - }) - .unwrap_or(vec![]), - ..Default::default() - }) - .into(); - } - try_activate_screen(); - match super::display_service::update_get_sync_displays().await { + match super::display_service::update_get_sync_displays_on_login().await { Err(err) => { res.set_error(format!("{}", err)); } @@ -1301,13 +1307,25 @@ impl Connection { } pi.displays = displays; pi.current_display = self.display_idx as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: pi + .displays + .get(self.display_idx) + .map(|d| crate::platform::resolutions(&d.name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } res.set_peer_info(pi); sub_service = true; #[cfg(target_os = "linux")] { // use rdp_input when uinput is not available in wayland. Ex: flatpak - if !is_x11() && !crate::is_server() { + if input_service::wayland_use_rdp_input() { let _ = setup_rdp_input().await; } } @@ -1354,7 +1372,7 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { + if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { @@ -1426,6 +1444,13 @@ impl Connection { self.clipboard && !self.disable_clipboard } + #[inline] + fn can_sub_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.peer_keyboard_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) != "Y" + } + fn audio_enabled(&self) -> bool { self.audio && !self.disable_audio } @@ -1469,6 +1494,9 @@ impl Connection { let mut msg_out = Message::new(); let mut res = LoginResponse::new(); res.set_error(err.to_string()); + if err.to_string() == crate::client::REQUIRE_2FA { + res.enable_trusted_devices = Self::enable_trusted_devices(); + } msg_out.set_login_response(res); self.send(msg_out).await; } @@ -1530,15 +1558,10 @@ impl Connection { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(password.clone()) { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), - Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, - last_recv_time: self.last_recv_time.clone(), - random_password: password, - tfa: false, - }, + raii::AuthedConnID::update_or_insert_session( + self.session_key(), + Some(password), + Some(false), ); return true; } @@ -1559,21 +1582,15 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .map(|s| s.to_owned()); // last_recv_time is a mutex variable shared with connection, can be updated lively. - if let Some(mut session) = session { - if session.name == self.lr.my_name - && session.session_id == self.lr.session_id - && !self.lr.password.is_empty() + if let Some(session) = session { + if !self.lr.password.is_empty() && (tfa && session.tfa || !tfa && self.validate_one_password(session.random_password.clone())) { - session.last_recv_time = self.last_recv_time.clone(); - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); + log::info!("is recent session"); return true; } } @@ -1610,11 +1627,32 @@ impl Connection { } } + #[inline] + fn enable_trusted_devices() -> bool { + config::option2bool( + keys::OPTION_ENABLE_TRUSTED_DEVICES, + &Config::get_option(keys::OPTION_ENABLE_TRUSTED_DEVICES), + ) + } + async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { self.options_in_login = Some(o.clone()); } + if self.require_2fa.is_some() && !lr.hwid.is_empty() && Self::enable_trusted_devices() { + let devices = Config::get_trusted_devices(); + if let Some(device) = devices.iter().find(|d| d.hwid == lr.hwid) { + if !device.outdate() + && device.id == lr.my_id + && device.name == lr.my_name + && device.platform == lr.my_platform + { + log::info!("2FA bypassed by trusted devices"); + self.require_2fa = None; + } + } + } self.video_ack_required = lr.video_ack_required; } @@ -1633,9 +1671,11 @@ impl Connection { .await { log::error!("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() && !err.to_string().contains(crate::platform::EXPLORER_EXE) + && !crate::hbbs_http::sync::is_pro() { allow_err!(tx_from_cm_clone.send(Data::CmErr(err.to_string()))); } @@ -1658,7 +1698,7 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER) { + if !Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER) { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -1731,7 +1771,9 @@ impl Connection { self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) .await; return false; - } else if password::approve_mode() == ApproveMode::Click + } else if (password::approve_mode() == ApproveMode::Click + && !(crate::platform::is_prelogin() + && crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y")) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); @@ -1799,34 +1841,21 @@ impl Connection { if res { self.update_failure(failure, true, 1); self.require_2fa.take(); + raii::AuthedConnID::set_session_2fa(self.session_key()); self.send_logon_response().await; self.try_start_cm( self.lr.my_id.to_owned(), self.lr.my_name.to_owned(), self.authorized, ); - let session = SESSIONS - .lock() - .unwrap() - .get(&self.lr.my_id) - .map(|s| s.to_owned()); - if let Some(mut session) = session { - session.tfa = true; - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); - } else { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), - Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, - last_recv_time: self.last_recv_time.clone(), - random_password: "".to_owned(), - tfa: true, - }, - ); + if !tfa.hwid.is_empty() && Self::enable_trusted_devices() { + Config::add_trusted_device(TrustedDevice { + hwid: tfa.hwid, + time: hbb_common::get_time(), + id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + platform: self.lr.my_platform.clone(), + }); } } else { self.update_failure(failure, false, 1); @@ -1850,7 +1879,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")] @@ -1941,8 +1969,6 @@ impl Connection { Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { - let is_press = (me.press || me.down) && !crate::is_modifier(&me); - let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) @@ -1958,6 +1984,9 @@ impl Connection { } .filter(crate::keyboard::is_modifier); + let is_press = + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()); + if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); @@ -1998,14 +2027,6 @@ impl Connection { } // https://github.com/rustdesk/rustdesk/issues/8633 MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); - // handle all down as press - // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which - // make sure all key are released - let is_press = if cfg!(target_os = "linux") { - (me.press || me.down) && !crate::is_modifier(&me) - } else { - me.press - }; let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { @@ -2022,6 +2043,16 @@ impl Connection { } .filter(crate::keyboard::is_modifier); + // handle all down as press + // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which + // make sure all key are released + // https://github.com/rustdesk/rustdesk/issues/6793 + let is_press = if cfg!(target_os = "linux") { + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()) + } else { + me.press + }; + if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); @@ -2049,8 +2080,10 @@ impl Connection { Some(message::Union::Clipboard(cb)) => { if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, None); - #[cfg(all(feature = "flutter", target_os = "android"))] + update_clipboard(vec![cb], ClipboardSide::Host); + // ios as the controlled side is actually not supported for now. + // The following code is only used to preserve the logic of handling text clipboard on mobile. + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -2068,7 +2101,17 @@ impl Connection { } } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); + } + } + Some(message::Union::MultiClipboards(_mcb)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(_mcb.clipboards, ClipboardSide::Host); } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } Some(message::Union::Cliprdr(_clip)) => { @@ -2086,7 +2129,36 @@ impl Connection { } return true; } + if crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + let mut job_id = None; + match &fa.union { + Some(file_action::Union::Send(s)) => { + job_id = Some(s.id); + } + Some(file_action::Union::RemoveFile(rf)) => { + job_id = Some(rf.id); + } + Some(file_action::Union::Rename(r)) => { + job_id = Some(r.id); + } + Some(file_action::Union::Create(c)) => { + job_id = Some(c.id); + } + Some(file_action::Union::RemoveDir(rd)) => { + job_id = Some(rd.id); + } + _ => {} + } + if let Some(job_id) = job_id { + self.send(fs::new_error(job_id, "one-way-file-transfer-tip", 0)) + .await; + return true; + } + } match fa.union { + Some(file_action::Union::ReadEmptyDirs(rd)) => { + self.read_empty_dirs(&rd.path, rd.include_hidden); + } Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); } @@ -2221,6 +2293,22 @@ impl Connection { job.confirm(&r); } } + Some(file_action::Union::Rename(r)) => { + self.send_fs(ipc::FS::Rename { + id: r.id, + path: r.path.clone(), + new_name: r.new_name.clone(), + }); + self.send_to_cm(ipc::Data::FileTransferLog(( + "rename".to_string(), + serde_json::to_string(&FileRenameLog { + conn_id: self.inner.id(), + path: r.path, + new_name: r.new_name, + }) + .unwrap_or_default(), + ))); + } _ => {} } } @@ -2301,7 +2389,10 @@ impl Connection { } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::check_remove_session( + self.inner.id(), + self.session_key(), + ); return false; } @@ -2650,7 +2741,7 @@ impl Connection { } } } else { - if let Err(e) = virtual_display_manager::plug_out_monitor(t.display) { + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false, true) { log::error!("Failed to plug out virtual display {}: {}", t.display, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", @@ -2836,7 +2927,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); } } @@ -2848,7 +2939,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); s.write().unwrap().subscribe( NAME_CURSOR, @@ -3061,7 +3152,15 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); + } + + fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + }); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { @@ -3215,6 +3314,22 @@ impl Connection { } } } + + #[inline] + fn session_key(&self) -> SessionKey { + SessionKey { + peer_id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + 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 + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3398,6 +3513,14 @@ struct FileActionLog { dir: bool, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileRenameLog { + conn_id: i32, + path: String, + new_name: String, +} + struct FileRemoveLogControl { conn_id: i32, instant: Instant, @@ -3694,25 +3817,30 @@ 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(id: i32, conn_type: AuthConnType) -> Self { - AUTHED_CONNS.lock().unwrap().push((id, conn_type)); + pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { + AUTHED_CONNS + .lock() + .unwrap() + .push((conn_id, conn_type, session_key)); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); - Self(id, conn_type) + if conn_type == AuthConnType::Remote { + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_open(conn_id); + } + Self(conn_id, conn_type) } fn check_wake_lock() { @@ -3737,14 +3865,94 @@ mod raii { .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) .count() } + + pub fn check_remove_session(conn_id: i32, key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let contains = lock.contains_key(&key); + if contains { + // No two remote connections with the same session key, just for ensure. + let is_remote = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.0 == conn_id && c.1 == 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); + if is_remote || !another_remote { + lock.remove(&key); + log::info!("remove session"); + } else { + // Keep the session if there is another remote connection with same peer_id and session_id. + log::info!("skip remove session"); + } + } + } + + pub fn update_or_insert_session( + key: SessionKey, + password: Option, + tfa: Option, + ) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + if let Some(password) = password { + session.random_password = password; + } + if let Some(tfa) = tfa { + session.tfa = tfa; + } + } else { + lock.insert( + key, + Session { + random_password: password.unwrap_or_default(), + tfa: tfa.unwrap_or_default(), + last_recv_time: Arc::new(Mutex::new(Instant::now())), + }, + ); + } + } + + pub fn set_session_2fa(key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + session.tfa = true; + } else { + lock.insert( + key, + Session { + last_recv_time: Arc::new(Mutex::new(Instant::now())), + random_password: "".to_owned(), + tfa: true, + }, + ); + } + } + + pub fn conn_type(&self) -> AuthConnType { + self.1 + } } impl Drop for AuthedConnID { fn drop(&mut self) { if self.1 == AuthConnType::Remote { 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.0 != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() diff --git a/src/server/display_service.rs b/src/server/display_service.rs index b4abdecfa4a5..98b42a5facea 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -1,4 +1,5 @@ use super::*; +use crate::common::SimpleCallOnReturn; #[cfg(target_os = "linux")] use crate::platform::linux::is_x11; #[cfg(windows)] @@ -7,6 +8,7 @@ use crate::virtual_display_manager; use hbb_common::get_version_number; use hbb_common::protobuf::MessageField; use scrap::Display; +use std::sync::atomic::{AtomicBool, Ordering}; // https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call @@ -29,6 +31,9 @@ lazy_static::lazy_static! { static ref SYNC_DISPLAYS: Arc> = Default::default(); } +// https://github.com/rustdesk/rustdesk/pull/8537 +static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false); + #[derive(Default)] struct SyncDisplaysInfo { displays: Vec, @@ -39,13 +44,17 @@ impl SyncDisplaysInfo { fn check_changed(&mut self, displays: Vec) { if self.displays.len() != displays.len() { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } for (i, d) in displays.iter().enumerate() { if d != &self.displays[i] { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } } @@ -60,6 +69,21 @@ impl SyncDisplaysInfo { } } +pub fn temp_ignore_displays_changed() -> SimpleCallOnReturn { + TEMP_IGNORE_DISPLAYS_CHANGED.store(true, std::sync::atomic::Ordering::Relaxed); + SimpleCallOnReturn { + b: true, + f: Box::new(move || { + // Wait for a while to make sure check_display_changed() is called + // after video service has sending its `SwitchDisplay` message(`try_broadcast_display_changed()`). + std::thread::sleep(Duration::from_millis(1000)); + TEMP_IGNORE_DISPLAYS_CHANGED.store(false, Ordering::Relaxed); + // Trigger the display changed message. + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + }), + } +} + // This function is really useful, though a duplicate check if display changed. // The video server will then send the following messages to the client: // 1. the supported resolutions of the {idx} display @@ -204,9 +228,11 @@ fn get_displays_msg() -> Option { fn run(sp: EmptyExtraFieldService) -> ResultType<()> { while sp.ok() { sp.snapshot(|sps| { - if sps.has_subscribes() { - SYNC_DISPLAYS.lock().unwrap().is_synced = false; - bail!("new subscriber"); + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + if sps.has_subscribes() { + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + bail!("new subscriber"); + } } Ok(()) })?; @@ -318,14 +344,18 @@ pub fn is_inited_msg() -> Option { None } -pub async fn update_get_sync_displays() -> ResultType> { +pub async fn update_get_sync_displays_on_login() -> ResultType> { #[cfg(target_os = "linux")] { if !is_x11() { return super::wayland::get_displays().await; } } - check_update_displays(&try_get_displays()?); + #[cfg(not(windows))] + let displays = display_service::try_get_displays(); + #[cfg(windows)] + let displays = display_service::try_get_displays_add_amyuni_headless(); + check_update_displays(&displays?); Ok(SYNC_DISPLAYS.lock().unwrap().displays.clone()) } @@ -433,7 +463,6 @@ pub fn try_get_displays_(add_amyuni_headless: bool) -> ResultType> // } let no_displays_v = no_displays(&displays); - virtual_display_manager::set_can_plug_out_all(!no_displays_v); if no_displays_v { log::debug!("no displays, create virtual display"); if let Err(e) = virtual_display_manager::plug_in_headless() { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index eabb8844e04b..c7f651e9ac72 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -16,7 +16,7 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; #[cfg(target_os = "linux")] -use scrap::wayland::pipewire::RDP_RESPONSE; +use scrap::wayland::pipewire::RDP_SESSION_INFO; use std::{ convert::TryFrom, ops::{Deref, DerefMut, Sub}, @@ -175,6 +175,22 @@ impl LockModesHandler { } } + #[cfg(target_os = "linux")] + fn sleep_to_ensure_locked(v: bool, k: enigo::Key, en: &mut Enigo) { + if wayland_use_uinput() { + // Sleep at most 500ms to ensure the lock state is applied. + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(10)); + if en.get_key_state(k) == v { + break; + } + } + } else if wayland_use_rdp_input() { + // We can't call `en.get_key_state(k)` because there's no api for this. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + #[cfg(any(target_os = "windows", target_os = "linux"))] fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self { let mut en = ENIGO.lock().unwrap(); @@ -183,12 +199,15 @@ impl LockModesHandler { let caps_lock_changed = event_caps_enabled != local_caps_enabled; if caps_lock_changed { en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_caps_enabled, enigo::Key::CapsLock, &mut en); } let mut num_lock_changed = false; + let mut event_num_enabled = false; if is_numpad_key { let local_num_enabled = en.get_key_state(enigo::Key::NumLock); - let event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); + event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); num_lock_changed = event_num_enabled != local_num_enabled; } else if is_legacy_mode(key_event) { #[cfg(target_os = "windows")] @@ -199,6 +218,8 @@ impl LockModesHandler { } if num_lock_changed { en.key_click(enigo::Key::NumLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_num_enabled, enigo::Key::NumLock, &mut en); } Self { @@ -236,6 +257,14 @@ impl LockModesHandler { #[cfg(any(target_os = "windows", target_os = "linux"))] impl Drop for LockModesHandler { fn drop(&mut self) { + // Do not change led state if is Wayland uinput. + // Because there must be a delay to ensure the lock state is applied on Wayland uinput, + // which may affect the user experience. + #[cfg(target_os = "linux")] + if wayland_use_uinput() { + return; + } + let mut en = ENIGO.lock().unwrap(); if self.caps_lock_changed { en.key_click(enigo::Key::CapsLock); @@ -385,6 +414,9 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { let displays = super::display_service::get_sync_displays(); + if displays.len() <= 1 { + return Ok(()); + } let disp_idx = crate::get_focused_display(displays); if let Some(disp_idx) = disp_idx.map(|id| id as i32) { if state.is_changed(disp_idx) { @@ -421,6 +453,26 @@ const MOUSE_ACTIVE_DISTANCE: i32 = 5; static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); +// https://github.com/rustdesk/rustdesk/issues/9729 +// 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 +// 1. Simulate mouse events +// 2. Simulate the legacy mode key events +// 3. Simulate the functioin key events, like LockScreen +#[inline] +#[cfg(target_os = "macos")] +fn enigo_ignore_flags() -> bool { + !LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst) +} +#[inline] +#[cfg(target_os = "macos")] +fn set_last_legacy_mode(v: bool) { + LAST_KEY_LEGACY_MODE.store(v, Ordering::SeqCst); + ENIGO.lock().unwrap().set_ignore_flags(!v); +} + pub fn try_start_record_cursor_pos() -> Option> { if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { return None; @@ -473,6 +525,19 @@ impl VirtualInputState { fn new() -> Option { VirtualInput::new( CGEventSourceStateID::CombinedSessionState, + // Note: `CGEventTapLocation::Session` will be affected by the mouse events. + // When we're simulating key events, then move the physical mouse, the key events will be affected. + // It looks like https://github.com/rustdesk/rustdesk/issues/9729#issuecomment-2432306822 + // 1. Press "Command" key in RustDesk + // 2. Move the physical mouse + // 3. Press "V" key in RustDesk + // Then the controlled side just prints "v" instead of pasting. + // + // Changing `CGEventTapLocation::Session` to `CGEventTapLocation::HID` fixes it. + // But we do not consider this as a bug, because it's not a common case, + // we consider only RustDesk operates the controlled side. + // + // https://developer.apple.com/documentation/coregraphics/cgeventtaplocation/ CGEventTapLocation::Session, ) .map(|virtual_input| Self { @@ -518,15 +583,25 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT #[cfg(target_os = "linux")] pub async fn setup_rdp_input() -> ResultType<(), Box> { let mut en = ENIGO.lock()?; - let rdp_res_lock = RDP_RESPONSE.lock()?; - let rdp_res = rdp_res_lock.as_ref().ok_or("RDP response is None")?; + let rdp_info_lock = RDP_SESSION_INFO.lock()?; + let rdp_info = rdp_info_lock.as_ref().ok_or("RDP session is None")?; - let keyboard = RdpInputKeyboard::new(rdp_res.conn.clone(), rdp_res.session.clone())?; + let keyboard = RdpInputKeyboard::new(rdp_info.conn.clone(), rdp_info.session.clone())?; en.set_custom_keyboard(Box::new(keyboard)); log::info!("RdpInput keyboard created"); - if let Some(stream) = rdp_res.streams.clone().into_iter().next() { - let mouse = RdpInputMouse::new(rdp_res.conn.clone(), rdp_res.session.clone(), stream)?; + if let Some(stream) = rdp_info.streams.clone().into_iter().next() { + let resolution = rdp_info + .resolution + .lock() + .unwrap() + .unwrap_or(stream.get_size()); + let mouse = RdpInputMouse::new( + rdp_info.conn.clone(), + rdp_info.session.clone(), + stream, + resolution, + )?; en.set_custom_mouse(Box::new(mouse)); log::info!("RdpInput mouse created"); } @@ -903,6 +978,8 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) { let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; let mut en = ENIGO.lock().unwrap(); + #[cfg(target_os = "macos")] + en.set_ignore_flags(enigo_ignore_flags()); #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); if evt_type == MOUSE_TYPE_DOWN { @@ -1445,17 +1522,27 @@ fn translate_keyboard_mode(evt: &KeyEvent) { en.key_sequence(seq); #[cfg(any(target_os = "linux", target_os = "windows"))] { - if get_modifier_state(Key::Shift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); - } - if get_modifier_state(Key::RightShift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + #[cfg(target_os = "windows")] + let simulate_win_hot_key = is_hot_key_modifiers_down(&mut en); + #[cfg(target_os = "linux")] + let simulate_win_hot_key = false; + if !simulate_win_hot_key { + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } } for chr in seq.chars() { // char in rust is 4 bytes. // But for this case, char comes from keyboard. We only need 2 bytes. #[cfg(target_os = "windows")] - rdev::simulate_unicode(chr as _).ok(); + if simulate_win_hot_key { + rdev::simulate_char(chr, true).ok(); + } else { + rdev::simulate_unicode(chr as _).ok(); + } #[cfg(target_os = "linux")] en.key_click(Key::Layout(chr)); } @@ -1480,6 +1567,17 @@ fn translate_keyboard_mode(evt: &KeyEvent) { } } +#[inline] +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down(en: &mut Enigo) -> bool { + en.get_key_state(Key::Control) + || en.get_key_state(Key::RightControl) + || en.get_key_state(Key::Alt) + || en.get_key_state(Key::RightAlt) + || en.get_key_state(Key::Meta) + || en.get_key_state(Key::RWin) +} + #[cfg(target_os = "windows")] fn simulate_win2win_hotkey(code: u32, down: bool) { let unicode: u16 = (code & 0x0000FFFF) as u16; @@ -1604,16 +1702,24 @@ pub fn handle_key_(evt: &KeyEvent) { } } _ => {} - }; + } match evt.mode.enum_value() { Ok(KeyboardMode::Map) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); map_keyboard_mode(evt); } Ok(KeyboardMode::Translate) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); translate_keyboard_mode(evt); } _ => { + // All key down events are started from here, + // so we can reset the flag of last legacy mode here. + #[cfg(target_os = "macos")] + set_last_legacy_mode(true); legacy_keyboard_mode(evt); } } @@ -1636,6 +1742,18 @@ async fn send_sas() -> ResultType<()> { Ok(()) } +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_uinput() -> bool { + !crate::platform::is_x11() && crate::is_server() +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_rdp_input() -> bool { + !crate::platform::is_x11() && !crate::is_server() +} + lazy_static::lazy_static! { static ref MODIFIER_MAP: HashMap = [ (ControlKey::Alt, Key::Alt), diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index ca86e48e7922..47d2f5789638 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); } diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index 1a0a64054b98..910a192761df 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::sync::Arc; pub mod client { + use hbb_common::platform::linux::is_kde; + use super::*; const EVDEV_MOUSE_LEFT: i32 = 272; @@ -67,6 +69,8 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), + scale: Option, } impl RdpInputMouse { @@ -74,11 +78,32 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), ) -> ResultType { + // https://github.com/rustdesk/rustdesk/pull/9019#issuecomment-2295252388 + // There may be a bug in Rdp input on Gnome util Ubuntu 24.04 (Gnome 46) + // + // eg. Resultion 800x600, Fractional scale: 200% (logic size: 400x300) + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.RemoteDesktop.html#:~:text=new%20pointer%20position-,in%20the%20streams%20logical%20coordinate%20space,-. + // Then (x,y) in `mouse_move_to()` and `mouse_move_relative()` should be scaled to the logic size(stream.get_size()), which is from (0,0) to (400,300). + // For Ubuntu 24.04(Gnome 46), (x,y) is restricted from (0,0) to (400,300), but the actual range in screen is: + // Logic coordinate from (0,0) to (200x150). + // Or physical coordinate from (0,0) to (400,300). + let scale = if is_kde() { + if resolution.0 == 0 || stream.get_size().0 == 0 { + Some(1.0f64) + } else { + Some(resolution.0 as f64 / stream.get_size().0 as f64) + } + } else { + None + }; Ok(Self { conn, session, stream, + resolution, + scale, }) } } @@ -93,24 +118,44 @@ pub mod client { } fn mouse_move_to(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion_absolute( &portal, &self.session, HashMap::new(), self.stream.path as u32, - x as f64, - y as f64, + x, + y, ); } fn mouse_move_relative(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion( &portal, &self.session, HashMap::new(), - x as f64, - y as f64, + x, + y, ); } fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType { diff --git a/src/server/uinput.rs b/src/server/uinput.rs index f36ad03362b4..894ce82f90d7 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -4,7 +4,11 @@ use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, AttributeSet, EventType, InputEvent, }; -use hbb_common::{allow_err, bail, log, tokio::{self, runtime::Runtime}, ResultType}; +use hbb_common::{ + allow_err, bail, log, + tokio::{self, runtime::Runtime}, + ResultType, +}; static IPC_CONN_TIMEOUT: u64 = 1000; static IPC_REQUEST_TIMEOUT: u64 = 1000; @@ -34,7 +38,10 @@ pub mod client { fn send_get_key_state(&mut self, data: Data) -> ResultType { self.rt.block_on(self.conn.send(&data))?; - match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) { + match self + .rt + .block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) + { Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { Ok(state) } @@ -171,7 +178,6 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; - use mouce::MouseActions; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -233,7 +239,7 @@ pub mod service { (enigo::Key::Select, evdev::Key::KEY_SELECT), (enigo::Key::Print, evdev::Key::KEY_PRINT), // (enigo::Key::Execute, evdev::Key::KEY_EXECUTE), - // (enigo::Key::Snapshot, evdev::Key::KEY_SNAPSHOT), + (enigo::Key::Snapshot, evdev::Key::KEY_SYSRQ), (enigo::Key::Insert, evdev::Key::KEY_INSERT), (enigo::Key::Help, evdev::Key::KEY_HELP), (enigo::Key::Sleep, evdev::Key::KEY_SLEEP), @@ -241,7 +247,7 @@ pub mod service { (enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK), (enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK), (enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA), - (enigo::Key::Apps, evdev::Key::KEY_CONTEXT_MENU), + (enigo::Key::Apps, evdev::Key::KEY_COMPOSE), // it's a little strange that the key is mapped to KEY_COMPOSE, not KEY_MENU (enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK), (enigo::Key::Add, evdev::Key::KEY_KPPLUS), (enigo::Key::Subtract, evdev::Key::KEY_KPMINUS), @@ -389,7 +395,7 @@ pub mod service { } else { match key { enigo::Key::Layout(c) => { - if let Some((k,is_shift)) = KEY_MAP_LAYOUT.get(&c) { + if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) { log::trace!("mapkey {:?}, get {:?}", &key, k); return Ok((k.clone(), is_shift.clone())); } @@ -425,13 +431,14 @@ pub mod service { allow_err!(keyboard.emit(&[down_event])); } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { - let down_event = InputEvent::new(EventType::KEY, *code - 8, 0); - allow_err!(keyboard.emit(&[down_event])); + let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); + allow_err!(keyboard.emit(&[up_event])); } DataKeyboard::KeyDown(key) => { if let Ok((k, is_shift)) = map_key(key) { if is_shift { - let down_event = InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + let down_event = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); allow_err!(keyboard.emit(&[down_event])); } let down_event = InputEvent::new(EventType::KEY, k.code(), 1); @@ -504,7 +511,7 @@ pub mod service { } } - fn handle_mouse(mouse: &mut mouce::nix::UInputMouseManager, data: &DataMouse) { + fn handle_mouse(mouse: &mut mouce::UInputMouseManager, data: &DataMouse) { log::trace!("handle_mouse {:?}", &data); match data { DataMouse::MoveTo(x, y) => { @@ -515,9 +522,9 @@ pub mod service { } DataMouse::Down(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -526,9 +533,9 @@ pub mod service { } DataMouse::Up(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -537,9 +544,9 @@ pub mod service { } DataMouse::Click(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -553,9 +560,9 @@ pub mod service { let mut length = *length; let scroll = if length < 0 { - mouce::common::ScrollDirection::Up + mouce::ScrollDirection::Up } else { - mouce::common::ScrollDirection::Down + mouce::ScrollDirection::Down }; if length < 0 { @@ -621,7 +628,7 @@ pub mod service { rng_y.0, rng_y.1 ); - let mut mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { + let mut mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { log::error!("Failed to create mouse, {}", e); @@ -650,7 +657,7 @@ pub mod service { rng_y.0, rng_y.1 ); - mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { + mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { log::error!("Failed to create mouse, {}", e); @@ -761,3 +768,346 @@ pub mod service { log::info!("stop uinput control service"); } } + +// https://github.com/emrebicer/mouce +mod mouce { + use std::{ + fs::File, + io::{Error, ErrorKind, Result}, + mem::size_of, + os::{ + raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort}, + unix::{fs::OpenOptionsExt, io::AsRawFd}, + }, + thread, + time::Duration, + }; + + pub const O_NONBLOCK: c_int = 2048; + + /// ioctl and uinput definitions + const UI_ABS_SETUP: c_ulong = 1075598596; + const UI_SET_EVBIT: c_ulong = 1074025828; + const UI_SET_KEYBIT: c_ulong = 1074025829; + const UI_SET_RELBIT: c_ulong = 1074025830; + const UI_SET_ABSBIT: c_ulong = 1074025831; + const UI_DEV_SETUP: c_ulong = 1079792899; + const UI_DEV_CREATE: c_ulong = 21761; + const UI_DEV_DESTROY: c_uint = 21762; + + pub const EV_KEY: c_int = 0x01; + pub const EV_REL: c_int = 0x02; + pub const EV_ABS: c_int = 0x03; + pub const REL_X: c_uint = 0x00; + pub const REL_Y: c_uint = 0x01; + pub const ABS_X: c_uint = 0x00; + pub const ABS_Y: c_uint = 0x01; + pub const REL_WHEEL: c_uint = 0x08; + pub const REL_HWHEEL: c_uint = 0x06; + pub const BTN_LEFT: c_int = 0x110; + pub const BTN_RIGHT: c_int = 0x111; + pub const BTN_MIDDLE: c_int = 0x112; + pub const BTN_SIDE: c_int = 0x113; + pub const BTN_EXTRA: c_int = 0x114; + pub const BTN_FORWARD: c_int = 0x115; + pub const BTN_BACK: c_int = 0x116; + pub const BTN_TASK: c_int = 0x117; + const SYN_REPORT: c_int = 0x00; + const EV_SYN: c_int = 0x00; + const BUS_USB: c_ushort = 0x03; + + /// uinput types + #[repr(C)] + struct UInputSetup { + id: InputId, + name: [c_char; UINPUT_MAX_NAME_SIZE], + ff_effects_max: c_ulong, + } + + #[repr(C)] + struct InputId { + bustype: c_ushort, + vendor: c_ushort, + product: c_ushort, + version: c_ushort, + } + + #[repr(C)] + pub struct InputEvent { + pub time: TimeVal, + pub r#type: c_ushort, + pub code: c_ushort, + pub value: c_int, + } + + #[repr(C)] + pub struct TimeVal { + pub tv_sec: c_ulong, + pub tv_usec: c_ulong, + } + + #[repr(C)] + pub struct UinputAbsSetup { + pub code: c_ushort, + pub absinfo: InputAbsinfo, + } + + #[repr(C)] + pub struct InputAbsinfo { + pub value: c_int, + pub minimum: c_int, + pub maximum: c_int, + pub fuzz: c_int, + pub flat: c_int, + pub resolution: c_int, + } + + extern "C" { + fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; + fn write(fd: c_int, buf: *mut InputEvent, count: usize) -> c_long; + } + + #[derive(Debug, Copy, Clone)] + pub enum MouseButton { + Left, + Middle, + Side, + Extra, + Right, + Back, + Forward, + Task, + } + + #[derive(Debug, Copy, Clone)] + pub enum ScrollDirection { + Up, + Down, + Right, + Left, + } + + const UINPUT_MAX_NAME_SIZE: usize = 80; + + pub struct UInputMouseManager { + uinput_file: File, + } + + impl UInputMouseManager { + pub fn new(rng_x: (i32, i32), rng_y: (i32, i32)) -> Result { + let manager = UInputMouseManager { + uinput_file: File::options() + .write(true) + .custom_flags(O_NONBLOCK) + .open("/dev/uinput")?, + }; + let fd = manager.uinput_file.as_raw_fd(); + unsafe { + // For press events (also needed for mouse movement) + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); + ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); + + // For mouse movement + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_ABSBIT, ABS_X); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_X as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_x.0, + maximum: rng_x.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + ioctl(fd, UI_SET_ABSBIT, ABS_Y); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_Y as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_y.0, + maximum: rng_y.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + ioctl(fd, UI_SET_RELBIT, REL_WHEEL); + ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); + } + + let mut usetup = UInputSetup { + id: InputId { + bustype: BUS_USB, + // Random vendor and product + vendor: 0x2222, + product: 0x3333, + version: 0, + }, + name: [0; UINPUT_MAX_NAME_SIZE], + ff_effects_max: 0, + }; + + let mut device_bytes: Vec = "mouce-library-fake-mouse" + .chars() + .map(|ch| ch as c_char) + .collect(); + + // Fill the rest of the name buffer with empty chars + for _ in 0..UINPUT_MAX_NAME_SIZE - device_bytes.len() { + device_bytes.push('\0' as c_char); + } + + usetup.name.copy_from_slice(&device_bytes); + + unsafe { + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + } + + // On UI_DEV_CREATE the kernel will create the device node for this + // device. We are inserting a pause here so that userspace has time + // to detect, initialize the new device, and can start listening to + // the event, otherwise it will not notice the event we are about to send. + thread::sleep(Duration::from_millis(300)); + + Ok(manager) + } + + /// Write the given event to the uinput file + fn emit(&self, r#type: c_int, code: c_int, value: c_int) -> Result<()> { + let mut event = InputEvent { + time: TimeVal { + tv_sec: 0, + tv_usec: 0, + }, + r#type: r#type as c_ushort, + code: code as c_ushort, + value, + }; + let fd = self.uinput_file.as_raw_fd(); + + unsafe { + let count = size_of::(); + let written_bytes = write(fd, &mut event, count); + if written_bytes == -1 || written_bytes != count as c_long { + return Err(Error::new( + ErrorKind::Other, + format!("failed while trying to write to a file"), + )); + } + } + + Ok(()) + } + + /// Syncronize the device + fn syncronize(&self) -> Result<()> { + self.emit(EV_SYN, SYN_REPORT, 0)?; + // Give uinput some time to update the mouse location, + // otherwise it fails to move the mouse on release mode + // A delay of 1 milliseconds seems to be enough for it + thread::sleep(Duration::from_millis(1)); + Ok(()) + } + + /// Move the mouse relative to the current position + fn move_relative_(&self, x: i32, y: i32) -> Result<()> { + // uinput does not move the mouse in pixels but uses `units`. I couldn't + // find information regarding to this uinput `unit`, but according to + // my findings 1 unit corresponds to exactly 2 pixels. + // + // To achieve the expected behavior; divide the parameters by 2 + // + // This seems like there is a bug in this crate, but the + // behavior is the same on other projects that make use of + // uinput. e.g. `ydotool`. When you try to move your mouse, + // it will move 2x further pixels + self.emit(EV_REL, REL_X as c_int, (x as f32 / 2.).ceil() as c_int)?; + self.emit(EV_REL, REL_Y as c_int, (y as f32 / 2.).ceil() as c_int)?; + self.syncronize() + } + + fn map_btn(button: &MouseButton) -> c_int { + match button { + MouseButton::Left => BTN_LEFT, + MouseButton::Right => BTN_RIGHT, + MouseButton::Middle => BTN_MIDDLE, + MouseButton::Side => BTN_SIDE, + MouseButton::Extra => BTN_EXTRA, + MouseButton::Forward => BTN_FORWARD, + MouseButton::Back => BTN_BACK, + MouseButton::Task => BTN_TASK, + } + } + + pub fn move_to(&self, x: usize, y: usize) -> Result<()> { + // // For some reason, absolute mouse move events are not working on uinput + // // (as I understand those events are intended for touch events) + // // + // // As a work around solution; first set the mouse to top left, then + // // call relative move function to simulate an absolute move event + //self.move_relative(i32::MIN, i32::MIN)?; + //self.move_relative(x as i32, y as i32) + + self.emit(EV_ABS, ABS_X as c_int, x as c_int)?; + self.emit(EV_ABS, ABS_Y as c_int, y as c_int)?; + self.syncronize() + } + + pub fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<()> { + self.move_relative_(x_offset, y_offset) + } + + pub fn press_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 1)?; + self.syncronize() + } + + pub fn release_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 0)?; + self.syncronize() + } + + pub fn click_button(&self, button: &MouseButton) -> Result<()> { + self.press_button(button)?; + self.release_button(button) + } + + pub fn scroll_wheel(&self, direction: &ScrollDirection) -> Result<()> { + let (code, scroll_value) = match direction { + ScrollDirection::Up => (REL_WHEEL, 1), + ScrollDirection::Down => (REL_WHEEL, -1), + ScrollDirection::Left => (REL_HWHEEL, -1), + ScrollDirection::Right => (REL_HWHEEL, 1), + }; + self.emit(EV_REL, code as c_int, scroll_value)?; + self.syncronize() + } + } + + impl Drop for UInputMouseManager { + fn drop(&mut self) { + let fd = self.uinput_file.as_raw_fd(); + unsafe { + // Destroy the device, the file is closed automatically by the File module + ioctl(fd, UI_DEV_DESTROY as c_ulong); + } + } + } +} diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 5d3aeca85ea6..108af9f4498b 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, display_idx: usize, support: bool) { + if let Some(display) = self.displays.get_mut(&display_idx) { + 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,371 @@ 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, display_idx: usize) { + self.displays.insert(display_idx, DisplayData::default()); + } + + pub fn remove_display(&mut self, display_idx: usize) { + self.displays.remove(&display_idx); + } + + pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) { + if let Some(display) = self.displays.get_mut(&display_idx) { + 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 7fd5dee1f82a..5bc58da45d32 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -51,7 +51,7 @@ 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, @@ -413,9 +413,8 @@ fn run(vs: VideoService) -> ResultType<()> { 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"), @@ -461,7 +460,7 @@ fn run(vs: VideoService) -> ResultType<()> { VIDEO_QOS .lock() .unwrap() - .set_support_abr(display_idx, encoder.support_abr()); + .set_support_changing_quality(display_idx, encoder.support_changing_quality()); log::info!("initial quality: {quality:?}"); if sp.is_option_true(OPTION_REFRESH) { @@ -486,32 +485,23 @@ fn run(vs: VideoService) -> ResultType<()> { let mut repeat_encode_counter = 0; let repeat_encode_max = 10; let mut encode_fail_counter = 0; + 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, + display_idx, + )?; if sp.is_option_true(OPTION_REFRESH) { let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); log::info!("switch to refresh"); @@ -540,7 +530,7 @@ fn run(vs: VideoService) -> ResultType<()> { VRamEncoder::set_fallback_gdi(display_idx, true); bail!("SWITCH"); } - check_privacy_mode_changed(&sp, c.privacy_mode_id)?; + check_privacy_mode_changed(&sp, display_idx, &c)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() @@ -574,8 +564,12 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } #[cfg(windows)] { @@ -629,8 +623,12 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } } } @@ -659,7 +657,7 @@ 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, c.privacy_mode_id)?; + 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() { @@ -682,6 +680,7 @@ struct Raii(usize); impl Raii { fn new(display_idx: usize) -> Self { + VIDEO_QOS.lock().unwrap().new_display(display_idx); Raii(display_idx) } } @@ -692,14 +691,14 @@ impl Drop for Raii { VRamEncoder::set_not_use(self.0, 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.0); } } fn setup_encoder( c: &CapturerInfo, display_idx: usize, - quality: Quality, + quality: f32, client_record: bool, record_incoming: bool, last_portable_service_running: bool, @@ -719,7 +718,7 @@ fn setup_encoder( ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(c.width, c.height, &codec_format, record_incoming); + let recorder = get_recorder(record_incoming, display_idx); 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)) @@ -728,7 +727,7 @@ fn setup_encoder( fn get_encoder_config( c: &CapturerInfo, _display_idx: usize, - quality: Quality, + quality: f32, record: bool, _portable_service: bool, ) -> EncoderCfg { @@ -801,12 +800,7 @@ fn get_encoder_config( } } -fn get_recorder( - width: usize, - height: usize, - codec_format: &CodecFormat, - record_incoming: bool, -) -> Arc>> { +fn get_recorder(record_incoming: bool, display: usize) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); #[cfg(not(windows))] @@ -825,10 +819,7 @@ fn get_recorder( server: true, id: Config::get_id(), dir: crate::ui_interface::video_save_directory(root), - filename: "".to_owned(), - width, - height, - format: codec_format.clone(), + display, tx, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) @@ -844,14 +835,15 @@ fn check_change_scale(hardware: bool) -> ResultType<()> { use hbb_common::config::keys::OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE as SCALE_SOFT; // isStart flag is set at the end of startCapture() in Android, wait it to be set. - for i in 0..6 { + let n = 60; // 3s + for i in 0..n { if scrap::is_start() == Some(true) { log::info!("start flag is set"); break; } log::info!("wait for start, {i}"); std::thread::sleep(Duration::from_millis(50)); - if i == 5 { + if i == n - 1 { log::error!("wait for start timeout"); } } @@ -876,9 +868,13 @@ fn check_change_scale(hardware: bool) -> ResultType<()> { Ok(()) } -fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> { +fn check_privacy_mode_changed( + sp: &GenericService, + display_idx: usize, + ci: &CapturerInfo, +) -> ResultType<()> { let privacy_mode_id_2 = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); - if privacy_mode_id != privacy_mode_id_2 { + if ci.privacy_mode_id != privacy_mode_id_2 { if privacy_mode_id_2 != INVALID_PRIVACY_MODE_CONN_ID { let msg_out = crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOnByOther, @@ -887,6 +883,7 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu sp.send_to_others(msg_out, privacy_mode_id_2); } log::info!("switch due to privacy mode changed"); + try_broadcast_display_changed(&sp, display_idx, ci, true).ok(); bail!("SWITCH"); } Ok(()) @@ -901,6 +898,9 @@ fn handle_one_frame( encoder: &mut Encoder, recorder: Arc>>, encode_fail_counter: &mut usize, + first_frame: &mut bool, + width: usize, + height: usize, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -912,6 +912,8 @@ fn handle_one_frame( })?; let mut send_conn_ids: HashSet = Default::default(); + let first = *first_frame; + *first_frame = false; match encoder.encode_to_message(frame, ms) { Ok(mut vf) => { *encode_fail_counter = 0; @@ -922,26 +924,33 @@ fn handle_one_frame( .lock() .unwrap() .as_mut() - .map(|r| r.write_message(&msg)); + .map(|r| r.write_message(&msg, width, height)); send_conn_ids = sp.send_video_frame(msg); } Err(e) => { + *encode_fail_counter += 1; + // Encoding errors are not frequent except on Android + if !cfg!(target_os = "android") { + log::error!("encode fail: {e:?}, times: {}", *encode_fail_counter,); + } let max_fail_times = if cfg!(target_os = "android") && encoder.is_hardware() { - 12 + 9 } else { - 6 + 3 }; - *encode_fail_counter += 1; - if *encode_fail_counter >= max_fail_times { + let repeat = !encoder.latency_free(); + // repeat encoders can reach max_fail_times on the first frame + if (first && !repeat) || *encode_fail_counter >= max_fail_times { *encode_fail_counter = 0; if encoder.is_hardware() { encoder.disable(); - log::error!("switch due to encoding fails more than {max_fail_times} times"); + log::error!("switch due to encoding fails, first frame: {first}, error: {e:?}"); bail!("SWITCH"); } } match e.to_string().as_str() { scrap::codec::ENCODE_NEED_SWITCH => { + encoder.disable(); log::error!("switch due to encoder need switch"); bail!("SWITCH"); } @@ -1042,3 +1051,40 @@ 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, + display_idx: usize, +) -> 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(display_idx, *send_counter); + *send_counter = 0; + } + drop(video_qos); + Ok(()) +} diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 12939320c90f..5560cb95ed3d 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -135,6 +135,7 @@ pub(super) async fn check_init() -> ResultType<()> { let mut maxx = 0; let mut miny = 0; let mut maxy = 0; + let use_uinput = crate::input_service::wayland_use_uinput(); if *CAP_DISPLAY_INFO.read().unwrap() == 0 { let mut lock = CAP_DISPLAY_INFO.write().unwrap(); @@ -167,28 +168,29 @@ pub(super) async fn check_init() -> ResultType<()> { num_cpus::get(), ); - let (max_width, max_height) = match get_max_desktop_resolution() { - Some(result) if !result.is_empty() => { - let resolution: Vec<&str> = result.split(" ").collect(); - let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); - let h: i32 = resolution[2] - .trim_end_matches(",") - .parse() - .unwrap_or(origin.1 + height as i32); - if w < origin.0 + width as i32 || h < origin.1 + height as i32 { - (origin.0 + width as i32, origin.1 + height as i32) + if use_uinput { + let (max_width, max_height) = match get_max_desktop_resolution() { + Some(result) if !result.is_empty() => { + let resolution: Vec<&str> = result.split(" ").collect(); + let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); + let h: i32 = resolution[2] + .trim_end_matches(",") + .parse() + .unwrap_or(origin.1 + height as i32); + if w < origin.0 + width as i32 || h < origin.1 + height as i32 { + (origin.0 + width as i32, origin.1 + height as i32) + } else { + (w, h) + } } - else{ - (w, h) - } - } - _ => (origin.0 + width as i32, origin.1 + height as i32), - }; + _ => (origin.0 + width as i32, origin.1 + height as i32), + }; - minx = 0; - maxx = max_width; - miny = 0; - maxy = max_height; + minx = 0; + maxx = max_width; + miny = 0; + maxy = max_height; + } let capturer = Box::into_raw(Box::new( Capturer::new(display).with_context(|| "Failed to create capturer")?, @@ -206,15 +208,17 @@ pub(super) async fn check_init() -> ResultType<()> { } } - if minx != maxx && miny != maxy { - log::info!( - "update mouse resolution: ({}, {}), ({}, {})", - minx, - maxx, - miny, - maxy - ); - allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await); + if use_uinput { + if minx != maxx && miny != maxy { + log::info!( + "update mouse resolution: ({}, {}), ({}, {})", + minx, + maxx, + miny, + maxy + ); + allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await); + } } } Ok(()) diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 000000000000..ce1855bdb8bf --- /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 18d6d7e91ad7..3a3ae92f37f1 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -9,10 +9,22 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; pub fn start_tray() { + if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { + #[cfg(target_os = "macos")] + { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + #[cfg(not(target_os = "macos"))] + { + return; + } + } allow_err!(make_tray()); } -pub fn make_tray() -> hbb_common::ResultType<()> { +fn make_tray() -> hbb_common::ResultType<()> { // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs use hbb_common::anyhow::Context; use tao::event_loop::{ControlFlow, EventLoopBuilder}; @@ -86,12 +98,11 @@ pub fn make_tray() -> hbb_common::ResultType<()> { crate::run_me::<&str>(vec![]).ok(); } #[cfg(target_os = "linux")] - if !std::process::Command::new("xdg-open") - .arg(&crate::get_uri_prefix()) - .spawn() - .is_ok() { - crate::run_me::<&str>(vec![]).ok(); + // Do not use "xdg-open", it won't read config + if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { + crate::run_me::<&str>(vec![]).ok(); + } } }; diff --git a/src/ui.rs b/src/ui.rs index aa36fc578ea0..27586a54fce5 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; @@ -312,6 +312,10 @@ impl UI { install_path() } + fn install_options(&self) -> String { + install_options() + } + fn get_socks(&self) -> Value { Value::from_iter(get_socks()) } @@ -683,6 +687,7 @@ impl sciter::EventHandler for UI { fn set_share_rdp(bool); fn is_installed_lower_version(); fn install_path(); + fn install_options(); fn goto_install(); fn is_process_trusted(bool); fn is_can_screen_recording(bool); diff --git a/src/ui/common.tis b/src/ui/common.tis index 0f154a74cba5..b6d2b8ee2874 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -268,7 +268,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= view.close(); return; } - handler.send2fa(res.code); + handler.send2fa(res.code, res.trust_this_device || false); msgbox("connecting", "Connecting...", "Logging in..."); }; } else if (type == "session-login" || type == "session-re-login") { diff --git a/src/ui/header.tis b/src/ui/header.tis index c4e765280970..4b634cf54c5a 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -152,10 +152,13 @@ class Header: Reactor.Component { } function renderKeyboardPop(){ + const is_map_mode_supported = handler.is_keyboard_mode_supported("map"); + const is_translate_mode_supported = handler.is_keyboard_mode_supported("translate"); return -
  • {svg_checkmark}{translate('Legacy mode')}
  • -
  • {svg_checkmark}{translate('Map mode')}
  • +
  • {svg_checkmark}{translate('Legacy mode')}
  • + { is_map_mode_supported &&
  • {svg_checkmark}{translate('Map mode')}
  • } + { is_translate_mode_supported &&
  • {svg_checkmark}{translate('Translate mode')}
  • }
    ; } @@ -298,26 +301,12 @@ class Header: Reactor.Component { } event click $(span#recording) (_, me) { - recording = !recording; header.update(); - handler.record_status(recording); - // 0 is just a dummy value. It will be ignored by the handler. - if (recording) { - handler.refresh_video(0); - if (handler.version_cmp(pi.version, '1.2.4') >= 0) handler.record_screen(recording, pi.current_display, display_width, display_height); - } - else { - handler.record_screen(recording, pi.current_display, display_width, display_height); - } + handler.record_screen(!recording) } event click $(#screen) (_, me) { if (pi.current_display == me.index) return; - if (recording) { - recording = false; - handler.record_screen(false, pi.current_display, display_width, display_height); - handler.record_status(false); - } handler.switch_display(me.index); } @@ -515,6 +504,7 @@ if (!(is_file_transfer || is_port_forward)) { handler.updatePi = function(v) { pi = v; + recording = handler.is_recording(); header.update(); if (is_port_forward) { view.windowState = View.WINDOW_MINIMIZED; @@ -679,3 +669,8 @@ handler.setConnectionType = function(secured, direct) { direct_connection: direct, }); } + +handler.updateRecordStatus = function(status) { + recording = status; + header.update(); +} \ No newline at end of file diff --git a/src/ui/index.tis b/src/ui/index.tis index b10df76233eb..2c9b0f9835bc 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -253,10 +253,12 @@ class Enhancements: Reactor.Component { var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_local_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; msgbox("custom-recording", translate('Recording'),
    {translate('Enable recording session')}
    {translate('Automatically record incoming sessions')}
    +
    {translate('Automatically record outgoing sessions')}
    {show_root_dir ?
    {translate("Incoming")}:  {root_dir}
    : ""}
    {translate(show_root_dir ? "Outgoing" : "Directory")}:  {user_dir}
    @@ -267,7 +269,8 @@ class Enhancements: Reactor.Component { if (!res) return; handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); - handler.set_option("video-save-directory", $(#folderPath).text); + handler.set_local_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : ''); + handler.set_local_option("video-save-directory", $(#folderPath).text); }); } this.toggleMenuState(); @@ -828,11 +831,12 @@ class PasswordEyeArea : Reactor.Component { render() { var method = handler.get_option('verification-method'); var mode= handler.get_option('approve-mode'); - var value = mode == 'click' || method == 'use-permanent-password' ? "-" : password_cache[0]; + var hide_one_time = mode == 'click' || method == 'use-permanent-password'; + var value = hide_one_time ? "-" : password_cache[0]; return
    - {svg_refresh_password} + {hide_one_time ? "" : svg_refresh_password}
    ; } diff --git a/src/ui/install.tis b/src/ui/install.tis index 3a7920bcfdea..fad4071233a6 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -6,13 +6,16 @@ var install_path = ""; class Install: Reactor.Component { function render() { + const install_options = JSON.parse(view.install_options()); + const desktop_icon = { checked: install_options?.DESKTOPSHORTCUTS == '0' ? false : true }; + const startmenu_shortcuts = { checked: install_options?.STARTMENUSHORTCUTS == '0' ? false : true }; return
    {translate('Installation')}
    {translate('Installation Path')} {": "}
    -
    {translate('Create start menu shortcuts')}
    -
    {translate('Create desktop icon')}
    +
    {translate('Create start menu shortcuts')}
    +
    {translate('Create desktop icon')}
    {translate('End-user license agreement')}
    {translate('agreement_tip')}
    diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index a8fa79ad4a5c..34f6b04432d7 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -66,9 +66,11 @@ class MsgboxComponent: Reactor.Component { } function get2faContent() { + var enable_trusted_devices = handler.get_enable_trusted_devices(); return
    {translate('enter-2fa-title')}
    + {enable_trusted_devices ?
    {translate('Trust this device')}
    : ""}
    ; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 93a796f4bd5c..0296d82bda56 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -335,6 +335,10 @@ impl InvokeUiSession for SciterHandler { } fn next_rgba(&self, _display: usize) {} + + fn update_record_status(&self, start: bool) { + self.call("updateRecordStatus", &make_args!(start)); + } } pub struct SciterSession(Session); @@ -433,7 +437,8 @@ impl sciter::EventHandler for SciterSession { fn is_port_forward(); fn is_rdp(); fn login(String, String, String, bool); - fn send2fa(String); + fn send2fa(String, bool); + fn get_enable_trusted_devices(); fn new_rdp(); fn send_mouse(i32, i32, i32, bool, bool, bool, bool); fn enter(String); @@ -477,8 +482,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(i32); - fn record_screen(bool, i32, i32, i32); - fn record_status(bool); + fn record_screen(bool); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); @@ -486,6 +490,7 @@ impl sciter::EventHandler for SciterSession { fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); fn get_keyboard_mode(); + fn is_keyboard_mode_supported(String); fn save_keyboard_mode(String); fn alternative_codecs(); fn change_prefer_codec(); @@ -494,13 +499,14 @@ impl sciter::EventHandler for SciterSession { fn close_voice_call(); fn version_cmp(String, String); fn set_selected_windows_session_id(String); + fn is_recording(); } } impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let force_relay = args.contains(&"--relay".to_string()); - let mut session: Session = Session { + let session: Session = Session { password: password.clone(), args, server_keyboard_enabled: Arc::new(RwLock::new(true)), @@ -523,7 +529,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None, None); + .initialize(id, conn_type, None, force_relay, None, None, None); Self(session) } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 7dae13ebc98c..a3373f8ccd72 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -1,16 +1,5 @@ -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::iter::FromIterator; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use std::sync::Arc; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicI64, Ordering}, - RwLock, - }, -}; - +#[cfg(target_os = "windows")] +use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] @@ -36,6 +25,18 @@ use hbb_common::{ #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use serde_derive::Serialize; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +use std::iter::FromIterator; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use std::sync::Arc; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; #[derive(Serialize, Clone)] pub struct Client { @@ -279,15 +280,6 @@ pub fn close(id: i32) { }; } -#[inline] -#[cfg(target_os = "android")] -pub fn notify_input_control(v: bool) { - for (_, mut client) in CLIENTS.write().unwrap().iter_mut() { - client.keyboard = v; - allow_err!(client.tx.send(Data::InputControl(v))); - } -} - #[inline] pub fn remove(id: i32) { CLIENTS.write().unwrap().remove(&id); @@ -311,6 +303,17 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { }; } +#[inline] +#[cfg(target_os = "android")] +pub fn switch_permission_all(name: String, enabled: bool) { + for (_, client) in CLIENTS.read().unwrap().iter() { + allow_err!(client.tx.send(Data::SwitchPermission { + name: name.clone(), + enabled + })); + } +} + #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_clients_state() -> String { @@ -439,7 +442,7 @@ impl IpcTaskRunner { Data::ClipboardFile(_clip) => { #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] { - let is_stopping_allowed = _clip.is_stopping_allowed_from_peer(); + 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); @@ -486,6 +489,45 @@ impl IpcTaskRunner { Data::CloseVoiceCall(reason) => { self.cm.voice_call_closed(self.conn_id, reason.as_str()); } + #[cfg(target_os = "windows")] + Data::ClipboardNonFile(_) => { + match crate::clipboard::check_clipboard_cm() { + Ok(multi_clipoards) => { + let mut raw_contents = bytes::BytesMut::new(); + let mut main_data = vec![]; + for c in multi_clipoards.clipboards.into_iter() { + let content_len = c.content.len(); + let (content, next_raw) = { + // TODO: find out a better threshold + if content_len > 1024 * 3 { + raw_contents.extend(c.content); + (bytes::Bytes::new(), true) + } else { + (c.content, false) + } + }; + main_data.push(ClipboardNonFile { + compress: c.compress, + content, + content_len, + next_raw, + width: c.width, + height: c.height, + format: c.format.value(), + special_name: c.special_name, + }); + } + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); + if !raw_contents.is_empty() { + allow_err!(self.stream.send_raw(raw_contents.into()).await); + } + } + Err(e) => { + log::debug!("Failed to get clipboard content. {}", e); + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); + } + } + } _ => { } @@ -529,7 +571,12 @@ impl IpcTaskRunner { if stop { ContextSend::set_is_stopped(); } else { - allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + 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 + // Don't call `ContextSend::set_is_stopped()`, because it will stop bidirectional file copy&paste. + } else { + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + } } } } @@ -587,7 +634,6 @@ pub async fn start_ipc(cm: ConnectionManager) { OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), )); - match ipc::new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { @@ -609,7 +655,7 @@ pub async fn start_ipc(cm: ConnectionManager) { log::error!("Failed to start cm ipc server: {}", err); } } - crate::platform::quit_gui(); + quit_cm(); } #[cfg(target_os = "android")] @@ -696,6 +742,12 @@ async fn handle_fs( use hbb_common::fs::serialize_transfer_job; match fs { + ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + } => { + read_empty_dirs(&dir, include_hidden, tx).await; + } ipc::FS::ReadDir { dir, include_hidden, @@ -845,10 +897,33 @@ async fn handle_fs( } } } + ipc::FS::Rename { id, path, new_name } => { + rename_file(path, new_name, id, tx).await; + } _ => {} } } +#[cfg(not(any(target_os = "ios")))] +async fn read_empty_dirs(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = dir.to_owned(); + let path_clone = dir.to_owned(); + + if let Ok(Ok(fds)) = + spawn_blocking(move || fs::get_empty_dirs_recursive(&path, include_hidden)).await + { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_empty_dirs(ReadEmptyDirsResponse { + path: path_clone, + empty_dirs: fds, + ..Default::default() + }); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + #[cfg(not(any(target_os = "ios")))] async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { let path = { @@ -909,6 +984,17 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { .await; } +#[cfg(not(any(target_os = "ios")))] +async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::rename_file(&path, &new_name)).await, + id, + 0, + tx, + ) + .await; +} + #[cfg(not(any(target_os = "ios")))] async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { let path = fs::get_path(&path); @@ -990,3 +1076,11 @@ pub fn close_voice_call(id: i32) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn quit_cm() { + // in case of std::process::exit not work + log::info!("quit cm"); + CLIENTS.write().unwrap().clear(); + crate::platform::quit_gui(); +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a192265df07a..323b651fedb3 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -47,6 +47,8 @@ pub struct UiStatus { pub mouse_time: i64, #[cfg(not(feature = "flutter"))] pub id: String, + #[cfg(feature = "flutter")] + pub video_conn_count: usize, } #[derive(Debug, Clone, Serialize)] @@ -65,14 +67,14 @@ lazy_static::lazy_static! { mouse_time: 0, #[cfg(not(feature = "flutter"))] id: "".to_owned(), + #[cfg(feature = "flutter")] + video_conn_count: 0, })); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); static ref ASYNC_HTTP_STATUS : Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } -pub static VIDEO_CONN_COUNT: AtomicUsize = AtomicUsize::new(0); - #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref OPTION_SYNCED: Arc> = Default::default(); @@ -173,21 +175,36 @@ pub fn get_option>(key: T) -> String { } #[inline] -#[cfg(target_os = "macos")] pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y" -} + #[cfg(target_os = "android")] + return false; + #[cfg(target_os = "ios")] + return false; -#[inline] -#[cfg(any(target_os = "windows", target_os = "linux"))] -pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" -} + #[cfg(target_os = "macos")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; -#[inline] -#[cfg(any(target_os = "android", target_os = "ios"))] -pub fn use_texture_render() -> bool { - false + #[cfg(target_os = "linux")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; + + #[cfg(target_os = "windows")] + { + if !cfg!(feature = "flutter") { + return false; + } + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 + #[cfg(debug_assertions)] + let default_texture = true; + #[cfg(not(debug_assertions))] + let default_texture = crate::platform::is_win_10_or_greater(); + if default_texture { + LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" + } else { + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + } + } } #[inline] @@ -206,18 +223,13 @@ pub fn get_hard_option(key: String) -> String { } #[inline] -pub fn get_buildin_option(key: &str) -> String { - config::BUILDIN_SETTINGS - .read() - .unwrap() - .get(key) - .cloned() - .unwrap_or_default() +pub fn get_builtin_option(key: &str) -> String { + crate::get_builtin_option(key) } #[inline] pub fn set_local_option(key: String, value: String) { - LocalConfig::set_option(key, value); + LocalConfig::set_option(key.clone(), value.clone()); } #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] @@ -326,6 +338,8 @@ pub fn get_sound_inputs() -> Vec { fn get_sound_inputs_() -> Vec { let mut out = Vec::new(); use cpal::traits::{DeviceTrait, HostTrait}; + // Do not use `cpal::host_from_id(cpal::HostId::ScreenCaptureKit)` for feature = "screencapturekit" + // Because we explicitly handle the "System Sound" device. let host = cpal::default_host(); if let Ok(devices) = host.devices() { for device in devices { @@ -426,6 +440,14 @@ pub fn install_path() -> String { return "".to_owned(); } +#[inline] +pub fn install_options() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_options(); + #[cfg(not(windows))] + return "{}".to_owned(); +} + #[inline] pub fn get_socks() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -684,7 +706,6 @@ pub fn create_shortcut(_id: String) { #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn discover() { - #[cfg(not(any(target_os = "ios")))] std::thread::spawn(move || { allow_err!(crate::lan::discover()); }); @@ -781,6 +802,7 @@ pub fn http_request(url: String, method: String, body: Option, header: S current_request.lock().unwrap().insert(url, res); }); } + #[inline] pub fn get_async_http_status(url: String) -> Option { match ASYNC_HTTP_STATUS.lock().unwrap().get(&url) { @@ -841,7 +863,11 @@ pub fn video_save_directory(root: bool) -> String { return dir.to_string_lossy().to_string(); } } - let dir = Config::get_option("video-save-directory"); + // Get directory from config file otherwise --server will use the old value from global var. + #[cfg(any(target_os = "linux", target_os = "macos"))] + let dir = LocalConfig::get_option_from_file(OPTION_VIDEO_SAVE_DIRECTORY); + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let dir = LocalConfig::get_option(OPTION_VIDEO_SAVE_DIRECTORY); if !dir.is_empty() { return dir; } @@ -1120,6 +1146,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { log::error!("ipc connection closed: {}", err); + if is_cm { + crate::ui_cm_interface::quit_cm(); + } break; } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1176,8 +1208,9 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { - VIDEO_CONN_COUNT.store(n, Ordering::Relaxed); + video_conn_count = n; } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, _c))))) => { if x > 0 { @@ -1195,6 +1228,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver {} @@ -1208,6 +1243,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return String::default(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_unlock_pin(); +} + +#[cfg(feature = "flutter")] +pub fn set_unlock_pin(pin: String) -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return String::default(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match ipc::set_unlock_pin(pin, true) { + Ok(_) => String::default(), + Err(err) => err.to_string(), + } +} + +#[cfg(feature = "flutter")] +pub fn get_trusted_devices() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_trusted_devices_json(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_trusted_devices(); +} + +#[cfg(feature = "flutter")] +pub fn remove_trusted_devices(json: &str) { + let hwids = serde_json::from_str::>(json).unwrap_or_default(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::remove_trusted_devices(&hwids); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::remove_trusted_devices(hwids); +} + +#[cfg(feature = "flutter")] +pub fn clear_trusted_devices() { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::clear_trusted_devices(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::clear_trusted_devices(); +} + +#[cfg(feature = "flutter")] +pub fn max_encrypt_len() -> usize { + hbb_common::config::ENCRYPT_MAX_LEN +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 02fdf1caa1ab..f76bd4e944ae 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -35,8 +35,8 @@ use hbb_common::{ use crate::client::io_loop::Remote; use crate::client::{ check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, - input_os_password, send_mouse, send_pointer_device_event, start_video_audio_threads, - FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, + input_os_password, send_mouse, send_pointer_device_event, FileManager, Key, LoginConfigHandler, + QualityStatus, KEY_MAP, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::GrabState; @@ -252,6 +252,18 @@ impl Session { self.fallback_keyboard_mode() } + pub fn is_keyboard_mode_supported(&self, mode: String) -> bool { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + crate::common::is_keyboard_mode_supported( + &mode, + self.get_peer_version(), + &self.peer_platform(), + ) + } else { + false + } + } + pub fn save_keyboard_mode(&self, value: String) { self.lc.write().unwrap().save_keyboard_mode(value); } @@ -342,7 +354,7 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn is_text_clipboard_required(&self) -> bool { *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() @@ -377,22 +389,12 @@ impl Session { self.send(Data::Message(LoginConfigHandler::refresh())); } - pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) { - self.send(Data::RecordScreen( - start, - display as usize, - w, - h, - self.get_id(), - )); + pub fn record_screen(&self, start: bool) { + self.send(Data::RecordScreen(start)); } - pub fn record_status(&self, status: bool) { - let mut misc = Misc::new(); - misc.set_client_record_status(status); - let mut msg = Message::new(); - msg.set_misc(misc); - self.send(Data::Message(msg)); + pub fn is_recording(&self) -> bool { + self.lc.read().unwrap().record_state } pub fn save_custom_image_quality(&self, custom_image_quality: i32) { @@ -519,10 +521,7 @@ impl Session { #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_xfce(&self) -> bool { - #[cfg(not(any(target_os = "ios")))] - return crate::platform::is_xfce(); - #[cfg(any(target_os = "ios"))] - false + crate::platform::is_xfce() } pub fn remove_port_forward(&self, port: i32) { @@ -788,7 +787,7 @@ impl Session { } #[cfg(any(target_os = "ios"))] - pub fn handle_flutter_key_event( + pub fn handle_flutter_raw_key_event( &self, _keyboard_mode: &str, _name: &str, @@ -800,7 +799,7 @@ impl Session { } #[cfg(not(any(target_os = "ios")))] - pub fn handle_flutter_key_event( + pub fn handle_flutter_raw_key_event( &self, keyboard_mode: &str, name: &str, @@ -812,7 +811,7 @@ impl Session { if name == "flutter_key" { self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up); } else { - self._handle_key_non_flutter_simulation( + self._handle_raw_key_non_flutter_simulation( keyboard_mode, platform_code, position_code, @@ -823,6 +822,65 @@ impl Session { } #[cfg(not(any(target_os = "ios")))] + fn _handle_raw_key_non_flutter_simulation( + &self, + keyboard_mode: &str, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if position_code < 0 || platform_code < 0 { + return; + } + let platform_code: u32 = platform_code as _; + let position_code: KeyCode = position_code as _; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(position_code) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(platform_code, position_code); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: SystemTime::now(), + unicode: None, + platform_code, + position_code: position_code as _, + event_type, + usb_hid: 0, + #[cfg(any(target_os = "windows", target_os = "macos"))] + extra_data: 0, + }; + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); + } + + pub fn handle_flutter_key_event( + &self, + keyboard_mode: &str, + character: &str, + usb_hid: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if character == "flutter_key" { + self._handle_key_flutter_simulation(keyboard_mode, usb_hid, down_or_up); + } else { + self._handle_key_non_flutter_simulation( + keyboard_mode, + character, + usb_hid, + lock_modes, + down_or_up, + ); + } + } + fn _handle_key_flutter_simulation( &self, _keyboard_mode: &str, @@ -831,10 +889,10 @@ impl Session { ) { // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/keyboard_key.g.dart#L4356 let ctrl_key = match platform_code { - 0x0007007f => Some(ControlKey::VolumeMute), - 0x00070080 => Some(ControlKey::VolumeUp), - 0x00070081 => Some(ControlKey::VolumeDown), - 0x00070066 => Some(ControlKey::Power), + 0x007f => Some(ControlKey::VolumeMute), + 0x0080 => Some(ControlKey::VolumeUp), + 0x0081 => Some(ControlKey::VolumeDown), + 0x0066 => Some(ControlKey::Power), _ => None, }; let Some(ctrl_key) = ctrl_key else { return }; @@ -847,26 +905,41 @@ impl Session { self.send_key_event(&key_event); } - #[cfg(not(any(target_os = "ios")))] fn _handle_key_non_flutter_simulation( &self, keyboard_mode: &str, - platform_code: i32, - position_code: i32, + character: &str, + usb_hid: i32, lock_modes: i32, down_or_up: bool, ) { - if position_code < 0 || platform_code < 0 { - return; - } - let platform_code: u32 = platform_code as _; - let position_code: KeyCode = position_code as _; + let key = rdev::usb_hid_key_from_code(usb_hid as _); + + #[cfg(any(target_os = "android", target_os = "ios"))] + let position_code: KeyCode = 0; + #[cfg(any(target_os = "android", target_os = "ios"))] + let platform_code: KeyCode = 0; - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_code(position_code) as rdev::Key; - // Windows requires special handling #[cfg(target_os = "windows")] - let key = rdev::get_win_key(platform_code, position_code); + let platform_code: u32 = rdev::win_code_from_key(key).unwrap_or(0); + #[cfg(target_os = "windows")] + let position_code: KeyCode = rdev::win_scancode_from_key(key).unwrap_or(0) as _; + + #[cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))] + let position_code: KeyCode = rdev::code_from_key(key).unwrap_or(0) as _; + #[cfg(not(any( + target_os = "windows", + target_os = "android", + target_os = "ios", + target_os = "linux" + )))] + let platform_code: u32 = position_code as _; + // For translate mode. + // We need to set the platform code (keysym) if is AltGr. + // https://github.com/rustdesk/rustdesk/blob/07cf1b4db5ef2f925efd3b16b87c33ce03c94809/src/keyboard.rs#L1029 + // https://github.com/flutter/flutter/issues/153811 + #[cfg(target_os = "linux")] + let platform_code: u32 = position_code as _; let event_type = if down_or_up { KeyPress(key) @@ -875,14 +948,27 @@ impl Session { }; let event = Event { time: SystemTime::now(), - unicode: None, + unicode: if character.is_empty() { + None + } else { + Some(rdev::UnicodeInfo { + name: Some(character.to_string()), + unicode: character.encode_utf16().collect(), + // is_dead: is not correct here, because flutter cannot detect deadcode for now. + is_dead: false, + }) + }, platform_code, position_code: position_code as _, event_type, + #[cfg(any(target_os = "android", target_os = "ios"))] + usb_hid: usb_hid as _, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + usb_hid: 0, #[cfg(any(target_os = "windows", target_os = "macos"))] extra_data: 0, }; - keyboard::client::process_event(keyboard_mode, &event, Some(lock_modes)); + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); } // flutter only TODO new input @@ -1156,15 +1242,29 @@ impl Session { self.send(Data::Login((os_username, os_password, password, remember))); } - pub fn send2fa(&self, code: String) { + pub fn send2fa(&self, code: String, trust_this_device: bool) { let mut msg_out = Message::new(); + let hwid = if trust_this_device { + crate::get_hwid() + } else { + Bytes::new() + }; + self.lc.write().unwrap().set_option( + "trust-this-device".to_string(), + if trust_this_device { "Y" } else { "" }.to_string(), + ); msg_out.set_auth_2fa(Auth2FA { code, + hwid, ..Default::default() }); self.send(Data::Message(msg_out)); } + pub fn get_enable_trusted_devices(&self) -> bool { + self.lc.read().unwrap().enable_trusted_devices + } + pub fn new_rdp(&self) { self.send(Data::NewRDP); } @@ -1382,6 +1482,10 @@ impl Session { msg.set_misc(misc); self.send(Data::Message(msg)); } + + pub fn get_conn_token(&self) -> Option { + self.lc.read().unwrap().get_conn_token() + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1445,6 +1549,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_current_display(&self, disp_idx: i32); #[cfg(feature = "flutter")] fn is_multi_ui_session(&self) -> bool; + fn update_record_status(&self, start: bool); + fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} } impl Deref for Session { @@ -1633,18 +1739,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")))] @@ -1738,40 +1832,7 @@ pub async fn io_loop(handler: Session, round: u32) { } return; } - let frame_count_map: Arc>> = Default::default(); - let frame_count_map_cl = frame_count_map.clone(); - let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender, video_queue_map, decode_fps, chroma) = - start_video_audio_threads( - handler.clone(), - move |display: usize, - data: &mut scrap::ImageRgb, - _texture: *mut c_void, - pixelbuffer: bool| { - let mut write_lock = frame_count_map_cl.write().unwrap(); - let count = write_lock.get(&display).unwrap_or(&0) + 1; - write_lock.insert(display, count); - drop(write_lock); - if pixelbuffer { - ui_handler.on_rgba(display, data); - } else { - #[cfg(all(feature = "vram", feature = "flutter"))] - ui_handler.on_texture(display, _texture); - } - }, - ); - - let mut remote = Remote::new( - handler, - video_queue_map, - video_sender, - audio_sender, - receiver, - sender, - frame_count_map, - decode_fps, - chroma, - ); + let mut remote = Remote::new(handler, receiver, sender); remote.io_loop(&key, &token, round).await; remote.sync_jobs_status_to_local().await; } diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 7807bd3d548f..b2791767e884 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -1,5 +1,4 @@ use hbb_common::{bail, platform::windows::is_windows_version_or_greater, ResultType}; -use std::sync::atomic; // This string is defined here. // https://github.com/rustdesk-org/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40 @@ -9,29 +8,7 @@ pub const AMYUNI_IDD_DEVICE_STRING: &'static str = "USB Mobile Monitor Virtual D const IDD_IMPL: &str = IDD_IMPL_AMYUNI; const IDD_IMPL_RUSTDESK: &str = "rustdesk_idd"; const IDD_IMPL_AMYUNI: &str = "amyuni_idd"; - -const IS_CAN_PLUG_OUT_ALL_NOT_SET: i8 = 0; -const IS_CAN_PLUG_OUT_ALL_YES: i8 = 1; -const IS_CAN_PLUG_OUT_ALL_NO: i8 = 2; -static IS_CAN_PLUG_OUT_ALL: atomic::AtomicI8 = atomic::AtomicI8::new(IS_CAN_PLUG_OUT_ALL_NOT_SET); - -pub fn is_can_plug_out_all() -> bool { - IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) != IS_CAN_PLUG_OUT_ALL_NO -} - -// No need to consider concurrency here. -pub fn set_can_plug_out_all(v: bool) { - if IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) == IS_CAN_PLUG_OUT_ALL_NOT_SET { - IS_CAN_PLUG_OUT_ALL.store( - if v { - IS_CAN_PLUG_OUT_ALL_YES - } else { - IS_CAN_PLUG_OUT_ALL_NO - }, - atomic::Ordering::Relaxed, - ); - } -} +const IDD_PLUG_OUT_ALL_INDEX: i32 = -1; pub fn is_amyuni_idd() -> bool { IDD_IMPL == IDD_IMPL_AMYUNI @@ -100,17 +77,17 @@ pub fn plug_in_monitor(idx: u32, modes: Vec) -> Re } } -pub fn plug_out_monitor(index: i32) -> ResultType<()> { +pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => { - let indices = if index == -1 { + let indices = if index == IDD_PLUG_OUT_ALL_INDEX { rustdesk_idd::get_virtual_displays() } else { vec![index as _] }; rustdesk_idd::plug_out_peer_request(&indices) } - IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index), + IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all, force_one), _ => bail!("Unsupported virtual display implementation."), } } @@ -126,12 +103,16 @@ pub fn plug_in_peer_request(modes: Vec>) -> Re } } -pub fn plug_out_monitor_indices(indices: &[u32]) -> ResultType<()> { +pub fn plug_out_monitor_indices( + indices: &[u32], + force_all: bool, + force_one: bool, +) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::plug_out_peer_request(indices), IDD_IMPL_AMYUNI => { for _idx in indices.iter() { - amyuni_idd::plug_out_monitor(0)?; + amyuni_idd::plug_out_monitor(0, force_all, force_one)?; } Ok(()) } @@ -142,7 +123,7 @@ pub fn plug_out_monitor_indices(indices: &[u32]) -> ResultType<()> { pub fn reset_all() -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::reset_all(), - IDD_IMPL_AMYUNI => crate::privacy_mode::turn_off_privacy(0, None).unwrap_or(Ok(())), + IDD_IMPL_AMYUNI => amyuni_idd::reset_all(), _ => bail!("Unsupported virtual display implementation."), } } @@ -402,11 +383,11 @@ pub mod rustdesk_idd { pub mod amyuni_idd { use super::windows; - use crate::platform::win_device; + use crate::platform::{reg_display_settings, win_device}; use hbb_common::{bail, lazy_static, log, tokio::time::Instant, ResultType}; use std::{ ptr::null_mut, - sync::{Arc, Mutex}, + sync::{atomic, Arc, Mutex}, time::Duration, }; use winapi::{ @@ -429,6 +410,14 @@ pub mod amyuni_idd { static ref LOCK: Arc> = Default::default(); static ref LAST_PLUG_IN_HEADLESS_TIME: Arc>> = Arc::new(Mutex::new(None)); } + const VIRTUAL_DISPLAY_MAX_COUNT: usize = 4; + // The count of virtual displays plugged in. + // This count is not accurate, because: + // 1. The virtual display driver may also be controlled by other processes. + // 2. RustDesk may crash and restart, but the virtual displays are kept. + // + // to-do: Maybe a better way is to add an option asking the user if plug out all virtual displays on disconnect. + static VIRTUAL_DISPLAY_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0); fn get_deviceinstaller64_work_dir() -> ResultType>> { let cur_exe = std::env::current_exe()?; @@ -532,23 +521,60 @@ pub mod amyuni_idd { Ok(()) } + pub fn reset_all() -> ResultType<()> { + let _ = crate::privacy_mode::turn_off_privacy(0, None); + let _ = plug_out_monitor(super::IDD_PLUG_OUT_ALL_INDEX, true, false); + *LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap() = None; + Ok(()) + } + #[inline] - fn plug_monitor_(add: bool) -> Result<(), win_device::DeviceError> { + fn plug_monitor_( + add: bool, + wait_timeout: Option, + ) -> Result<(), win_device::DeviceError> { let cmd = if add { 0x10 } else { 0x00 }; let cmd = [cmd, 0x00, 0x00, 0x00]; + let now = Instant::now(); + let c1 = get_monitor_count(); unsafe { win_device::device_io_control(&INTERFACE_GUID, PLUG_MONITOR_IO_CONTROL_CDOE, &cmd, 0)?; } + if let Some(wait_timeout) = wait_timeout { + while now.elapsed() < wait_timeout { + if get_monitor_count() != c1 { + break; + } + std::thread::sleep(Duration::from_millis(30)); + } + } + // No need to consider concurrency here. + if add { + // If the monitor is plugged in, increase the count. + // Though there's already a check of `VIRTUAL_DISPLAY_MAX_COUNT`, it's still better to check here for double ensure. + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) < VIRTUAL_DISPLAY_MAX_COUNT { + VIRTUAL_DISPLAY_COUNT.fetch_add(1, atomic::Ordering::SeqCst); + } + } else { + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) > 0 { + VIRTUAL_DISPLAY_COUNT.fetch_sub(1, atomic::Ordering::SeqCst); + } + } Ok(()) } // `std::thread::sleep()` with a timeout is acceptable here. // Because user can wait for a while to plug in a monitor. - fn plug_in_monitor_(add: bool, is_driver_async_installed: bool) -> ResultType<()> { + fn plug_in_monitor_( + add: bool, + is_driver_async_installed: bool, + wait_timeout: Option, + ) -> ResultType<()> { let timeout = Duration::from_secs(3); let now = Instant::now(); + let reg_connectivity_old = reg_display_settings::read_reg_connectivity(); loop { - match plug_monitor_(add) { + match plug_monitor_(add, wait_timeout) { Ok(_) => { break; } @@ -567,9 +593,36 @@ pub mod amyuni_idd { } } } + // Workaround for the issue that we can't set the default the resolution. + if let Ok(old_connectivity_old) = reg_connectivity_old { + std::thread::spawn(move || { + try_reset_resolution_on_first_plug_in(old_connectivity_old.len(), 1920, 1080); + }); + } + Ok(()) } + fn try_reset_resolution_on_first_plug_in( + old_connectivity_len: usize, + width: usize, + height: usize, + ) { + for _ in 0..10 { + std::thread::sleep(Duration::from_millis(300)); + if let Ok(reg_connectivity_new) = reg_display_settings::read_reg_connectivity() { + if reg_connectivity_new.len() != old_connectivity_len { + for name in + windows::get_device_names(Some(super::AMYUNI_IDD_DEVICE_STRING)).iter() + { + crate::platform::change_resolution(&name, width, height).ok(); + } + break; + } + } + } + } + pub fn plug_in_headless() -> ResultType<()> { let mut tm = LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap(); if let Some(tm) = &mut *tm { @@ -586,7 +639,7 @@ pub mod amyuni_idd { bail!("Failed to install driver."); } - plug_in_monitor_(true, is_async) + plug_in_monitor_(true, is_async, Some(Duration::from_millis(3_000))) } pub fn plug_in_monitor() -> ResultType<()> { @@ -596,46 +649,75 @@ pub mod amyuni_idd { bail!("Failed to install driver."); } - if get_monitor_count() == 4 { - bail!("There are already 4 monitors plugged in."); + if get_monitor_count() == VIRTUAL_DISPLAY_MAX_COUNT { + bail!("There are already {VIRTUAL_DISPLAY_MAX_COUNT} monitors plugged in."); } - plug_in_monitor_(true, is_async) + plug_in_monitor_(true, is_async, None) } - pub fn plug_out_monitor(index: i32) -> ResultType<()> { - let all_count = windows::get_device_names(None).len(); + // `index` the display index to plug out. -1 means plug out all. + // `force_all` is used to forcibly plug out all virtual displays. + // `force_one` is used to forcibly plug out one virtual display managed by other processes + // if there're no virtual displays managed by RustDesk. + pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { + let plug_out_all = index == super::IDD_PLUG_OUT_ALL_INDEX; + // If `plug_out_all and force_all` is true, forcibly plug out all virtual displays. + // Though the driver may be controlled by other processes, + // we still forcibly plug out all virtual displays. + // + // 1. RustDesk plug in 2 virtual displays. (RustDesk) + // 2. Other process plug out all virtual displays. (User mannually) + // 3. Other process plug in 1 virtual display. (User mannually) + // 4. RustDesk plug out all virtual displays in this call. (RustDesk disconnect) + // + // This is not a normal scenario, RustDesk will plug out virtual display unexpectedly. + let mut plug_in_count = VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::Relaxed); let amyuni_count = get_monitor_count(); + if !plug_out_all { + if plug_in_count == 0 && amyuni_count > 0 { + if force_one { + plug_in_count = 1; + } else { + bail!("The virtual display is managed by other processes."); + } + } + } else { + // Ignore the message if trying to plug out all virtual displays. + } + + let all_count = windows::get_device_names(None).len(); let mut to_plug_out_count = match all_count { 0 => return Ok(()), 1 => { - if amyuni_count == 0 { + if plug_in_count == 0 { bail!("No virtual displays to plug out.") } else { - if super::is_can_plug_out_all() { + if force_all { 1 } else { - bail!("This only virtual display cannot be pulled out.") + bail!("This only virtual display cannot be plugged out.") } } } _ => { - if all_count == amyuni_count { - if super::is_can_plug_out_all() { + if all_count == plug_in_count { + if force_all { all_count } else { all_count - 1 } } else { - amyuni_count + plug_in_count } } }; - if to_plug_out_count != 0 && index != -1 { + if to_plug_out_count != 0 && !plug_out_all { to_plug_out_count = 1; } + for _i in 0..to_plug_out_count { - let _ = plug_monitor_(false); + let _ = plug_monitor_(false, None); } Ok(()) } diff --git a/vcpkg.json b/vcpkg.json index f1d7036eb5f1..0b2b9ab4f9e7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -24,10 +24,6 @@ "name": "oboe", "platform": "android" }, - { - "name": "oboe-wrapper", - "platform": "android" - }, { "name": "opus", "host": true @@ -87,8 +83,17 @@ ] }, "overrides": [ - { "name": "ffnvcodec", "version": "11.1.5.2" }, - { "name": "amd-amf", "version": "1.4.29" }, - { "name": "mfx-dispatch", "version": "1.35.1" } + { + "name": "ffnvcodec", + "version": "12.1.14.0" + }, + { + "name": "amd-amf", + "version": "1.4.35" + }, + { + "name": "mfx-dispatch", + "version": "1.35.1" + } ] } \ No newline at end of file diff --git a/vdi/README.md b/vdi/README.md deleted file mode 100644 index 85e6ff194b98..000000000000 --- 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 70f9eaeb2704..000000000000 --- 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 ea8c4bf7f35f..000000000000 --- 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 0b2e8ca2b8d3..000000000000 --- 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 0584b469018d..000000000000 --- 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 0283266bf7dd..000000000000 --- 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 9f856fa2e5ec..000000000000 --- 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 a342f1a9afc0..000000000000 --- 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 e9f8d7ed3cfa..000000000000 --- 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 ea32a028a3c5..000000000000 --- 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 b43bd364f46b..000000000000 --- 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}"); - } - } -}