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

- Servers •
Build •
Docker •
Structure •
Snapshot
- [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk]
We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language
-Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!Caution]
+> **Misuse Disclaimer:**
+> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application.
+
+
+Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
-Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
+Yet another remote desktop solution, written in Rust. Works out of the box with no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).

@@ -25,9 +29,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
-[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](https://flathub.org/apps/com.rustdesk.RustDesk)
## Dependencies
@@ -39,7 +46,7 @@ Please download Sciter dynamic library yourself.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-## Raw steps to build
+## Raw Steps to build
- Prepare your Rust development env and C++ build env
@@ -52,7 +59,7 @@ Please download Sciter dynamic library yourself.
## [Build](https://rustdesk.com/docs/en/dev/build/)
-## How to build on Linux
+## How to Build on Linux
### Ubuntu 18 (Debian 10)
@@ -110,7 +117,7 @@ cd
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
-git clone https://github.com/rustdesk/rustdesk
+git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -125,6 +132,7 @@ Begin by cloning the repository and building the Docker container:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
+git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
@@ -146,7 +154,7 @@ Or, if you're running a release executable:
target/release/rustdesk
```
-Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
+Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
## File Structure
@@ -160,7 +168,7 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
-- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client
## Screenshots
@@ -172,6 +180,3 @@ Please ensure that you are running these commands from the root of the RustDesk

-## [Public Servers](#public-servers)
-
-RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml
index 98ceca3bb5e..c7b8cfee1d1 100644
--- a/appimage/AppImageBuilder-aarch64.yml
+++ b/appimage/AppImageBuilder-aarch64.yml
@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
- version: 1.3.5
- exec: usr/lib/rustdesk/rustdesk
+ version: 1.4.1
+ exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -77,7 +77,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11
- APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
+ APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test:
@@ -99,3 +99,4 @@ AppDir:
AppImage:
arch: aarch64
update-information: guess
+ comp: gzip
diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml
index 9ce7cc717e3..4025f1669ef 100644
--- a/appimage/AppImageBuilder-x86_64.yml
+++ b/appimage/AppImageBuilder-x86_64.yml
@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
- version: 1.3.5
- exec: usr/lib/rustdesk/rustdesk
+ version: 1.4.1
+ exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -80,7 +80,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11
- APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
+ APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
test:
@@ -102,3 +102,4 @@ AppDir:
AppImage:
arch: x86_64
update-information: guess
+ comp: gzip
diff --git a/build.py b/build.py
index 5d974092037..208b67359d0 100755
--- a/build.py
+++ b/build.py
@@ -9,6 +9,7 @@
import hashlib
import argparse
import sys
+from pathlib import Path
windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith(
@@ -296,8 +297,8 @@ def generate_control_file(version):
Priority: optional
Version: %s
Architecture: %s
-Maintainer: rustdesk
-Homepage: https://rustdesk.com
+Maintainer: B1 Systems GmbH
+Homepage: https://github.com/b1-systems/rustdesk
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.
@@ -321,7 +322,7 @@ def build_flutter_deb(version, features):
os.chdir('flutter')
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/etc/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
@@ -331,7 +332,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
- f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/')
+ f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -354,19 +355,19 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
system2('/bin/rm -rf ../res/DEBIAN/control')
- os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
+ os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch()))
os.chdir("..")
def build_deb_from_folder(version, binary_folder):
os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
@@ -374,7 +375,7 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
- f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/')
+ f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -391,12 +392,12 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
system2('/bin/rm -rf ../res/DEBIAN/control')
- os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
+ os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch()))
os.chdir("..")
@@ -404,12 +405,13 @@ def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(
- f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
+ f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
system2('flutter build macos --release')
+ system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
@@ -621,21 +623,24 @@ def main():
os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
- system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
- system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
- md5_file('etc/rustdesk/startwm.sh')
- md5_file('etc/X11/rustdesk/xorg.conf')
- md5_file('etc/pam.d/rustdesk')
- md5_file('usr/lib/rustdesk/libsciter-gtk.so')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
+ system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
+ system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
- system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
+ system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
+
+def md5_file_folder(base_dir):
+ base_path = Path(base_dir)
+ for file in base_path.rglob('*'):
+ if file.is_file() and 'DEBIAN' not in file.parts:
+ relative_path = file.relative_to(base_path)
+ md5_file(str(relative_path))
if __name__ == "__main__":
diff --git a/docs/CODE_OF_CONDUCT-NO.md b/docs/CODE_OF_CONDUCT-NO.md
new file mode 100644
index 00000000000..baefda0519a
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT-NO.md
@@ -0,0 +1,125 @@
+
+# Atferdskodeks for bidragsyterpaktern
+
+## Hva Vi Står For
+
+Vi som medlemer, bidragere, og ledere står for å skape ett hat-fritt felleskap,
+uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger,
+etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivå, utdanning,
+sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual
+identitet og orientasjon.
+
+Vi står for åpen, velkommende, mangfold, inklusiv og sunn oppførsel i vårt felleskap.
+
+## VÃ¥re Standarer
+
+Eksempler på oppførsel som hjelper ett positivt felleskap inkluderer:
+
+* Vise empati og vennlighet mot andre mennesker
+* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer
+* Gi og ta konstruktiv kritikk i beste mening
+* Akseptere ansvar og unskylde seg for de som er utsatt av våre feil,
+ og lære av disse
+* Fokusere på det som er best ikke bare for individer, men for felleskapet
+
+Eksempler på uakseptabel oppførsel inkluderer:
+
+* Bruk av seksualisert språk eller bilder, og seksual oppmerksomhet.
+* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep
+* Offentlig eller privat trakassering
+* Publisering av andres private informasjon, sånn som bosteds- og epost-addresser,
+ uten deres godskjenning.
+* Andre rettningslinjer som kan bli sett på som upassende i en profesjonell setting.
+
+## HÃ¥ndhevingsansvar
+
+Felleskapets ledere har ansvar for å klarifisere og håndheve våre standarer av
+akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons på
+oppførsel de anser som upassende, truende, fornermende eller skadelig.
+
+Felleskapets ledere har retten og ansvaret til å fjerne, redigere, eller avslå
+kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke
+samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for
+moderatorenes valg når passende.
+
+## Omfang
+
+Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og
+de gjelder også når ett individ representerer felleskapet på offentlige medier.
+Eksempler på representasjon av vårt felleskap inkluderer bruke av offisielle e-mail
+addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en
+utpekt representant på digitale og fysiske arrangsjemanger.
+
+## HÃ¥ndheving
+
+Hendelser av misbruk, trakasserende eller på noen måte uakseptert oppførsel kann
+bli raportert til felleskapets ledere med ansvar for håndheving på
+[info@rustdesk.com](mailto:info@rustdesk.com).
+All tilbakemelding vill bli sett gjennom og investigert rettferdig så fort som mulig.
+
+Alle felleskapets ledere er obligert til å respektere privatlivet og sikkerhetet ovenfor
+den som raporterer en hendelse.
+
+## HÃ¥ndhevings Guide
+
+Felleskapets ledere vill følge disse Rettningslinjene for sammfunspåvirkning med
+tanke på konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene:
+
+### 1. Korreksjon
+
+**Sammfunspåvirkning**: Bruk av upassende språk eller annen oppførsel ansett som
+uprofesjonelt eller uvelkommen i dette felleskapet.
+
+**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som
+klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig
+unskyldning kan bli forespurt.
+
+### 2. Advarsel
+
+**Sammfunspåvirkning**: Ett brudd på en singulær hendelse eller en serie handlinger.
+
+**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen
+interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
+de som håndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode.
+Dette inkluderer å unngå interaksjoner i felleskapets platformer, samt eksterne
+kanaler, som f.eks sosial media. Brudd av disse vilkårene kan føre til midlertidig
+eller permanent bannlysning.
+
+### 3. Midlertidig Bannlysning
+
+**Sammfunspåvirkning**: Ett særiøst brudd på felleskapets standarer, inkludert
+vedvarende upassende oppførsel.
+
+**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller
+offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen
+interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
+de som håndhever disse etiske rettningslinjene, er tillat for denne perioden.
+Brudd på disse vilkårene kan føre til permanent bannlysning.
+
+### 4. Permanent Bannlysning
+
+**Sammfunspåvirkning**: Demonstasjon av mønster i brudd på felleskapets standarer,
+inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller
+aggresjon mot eller nedsettelse av grupper individer.
+
+**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i
+felleskapet
+
+## Attribusjon
+
+Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage],
+versjon 2.0, tilgjengelig ved
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
+
+Sammfunspåvirknings guid inspirert av
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For svar til vanlige spørsmål angående disse etiske rettningslinjene, se FAQ på
+[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig
+ved [https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md
index 6258a9a7a11..b45c23d502e 100644
--- a/docs/CONTRIBUTING-DE.md
+++ b/docs/CONTRIBUTING-DE.md
@@ -1,42 +1,42 @@
-# Beiträge zu RustDesk
+# Beiträge zu RustDesk
-RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns
-helfen möchten:
+RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns
+helfen möchten:
-## Beiträge
+## Beiträge
-Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull
+Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull
Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur
-(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den
-Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle
-Beiträge sollten diesem Format folgen, auch die von Hauptakteuren.
+(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den
+Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle
+Beiträge sollten diesem Format folgen, auch die von Hauptakteuren.
-Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem
-Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert
-werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden.
+Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem
+Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert
+werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden.
-## Checkliste für Pull Requests
+## Checkliste für Pull Requests
-- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum
+- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum
aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das
- Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie
- möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten.
+ Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie
+ möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten.
-- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass
- jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte
- sich übersetzen lassen und Tests bestehen).
+- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass
+ jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte
+ sich übersetzen lassen und Tests bestehen).
-- Commits sollten von einem "Herkunftszertifikat für Entwickler"
+- Commits sollten von einem "Herkunftszertifikat für Entwickler"
(https://developercertificate.org) begleitet werden, das besagt, dass Sie (und
ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE)
- einverstanden sind. In Git ist dies die Option `-s` für `git commit`.
+ einverstanden sind. In Git ist dies die Option `-s` für `git commit`.
- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur
- Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine
- Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch
+ Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine
+ Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch
per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten.
-- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
+- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
Funktion beziehen.
Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow).
@@ -47,4 +47,4 @@ https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Kommunikation
-RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV).
+RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING-KR.md b/docs/CONTRIBUTING-KR.md
new file mode 100644
index 00000000000..5e432648eb2
--- /dev/null
+++ b/docs/CONTRIBUTING-KR.md
@@ -0,0 +1,46 @@
+# RustDesk 기여하기
+
+RustDesk는 ëª¨ë“ ë¶„ë“¤ì˜ ì°¸ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. ì €í¬ë¥¼ ë„와주실 ìƒê°ì´ 있으시다면
+ ë‹¤ìŒ ì§€ì¹¨ì„ ë”°ë¥´ì„¸ìš”:
+
+## 기여
+
+RustDesk ë˜ëŠ” ê·¸ 종ì†ì„±ì— 대한 기여는 GitHub í’€ 리퀘스트 형태로
+ì´ë£¨ì–´ì ¸ì•¼ 합니다. ê° í’€ 리퀘스트는 핵심 ê¸°ì—¬ìž (패치 ì ìš© 권한ì´
+있는 사람)ê°€ ê²€í† í•˜ì—¬ ë©”ì¸ íŠ¸ë¦¬ì— ì¶”ê°€í•˜ê±°ë‚˜ 필요한 변경 사í•ì—
+대한 í”¼ë“œë°±ì„ ì œê³µí•©ë‹ˆë‹¤. 핵심 기여ìžì˜ 기여를 í¬í•¨í•˜ì—¬ ëª¨ë“ ê¸°ì—¬ëŠ”
+ì´ í˜•ì‹ì„ ë”°ë¼ì•¼ 합니다.
+
+ì´ìŠˆì— ëŒ€í•´ ìž‘ì—…í•˜ê³ ì‹¶ìœ¼ì‹œë©´ ë¨¼ì € 해당 ì´ìŠˆì— ëŒ€í•´ ìž‘ì—…í•˜ê³ ì‹¶ë‹¤ëŠ”
+ëŒ“ê¸€ì„ ë‹¬ì•„ 해당 ì´ìŠˆë¥¼ ìš”ì²í•˜ì„¸ìš”. ì´ëŠ” ë™ì¼í•œ ì´ìŠˆì— ëŒ€í•œ 기여ìžì˜
+ì¤‘ë³µëœ ë…¸ë ¥ì„ ë°©ì§€í•˜ê¸° 위한 것입니다.
+
+## í’€ 리퀘스트 ì²´í¬ë¦¬ìŠ¤íŠ¸
+
+- Master 브랜치ì—서 브랜치를 ë§Œë“¤ê³ , 필요한 경우 í’€ 리퀘스트를 ì œì¶œí•˜ê¸°
+ ì „ì— í˜„ìž¬ 마스터 브랜치로 ë¦¬ë² ì´ìŠ¤í•˜ì„¸ìš”. 마스터 브랜치와 ê¹”ë”하게
+ 병합ë˜ì§€ 않으면 변경 사í•ì„ ë¦¬ë² ì´ìŠ¤í•˜ë¼ëŠ” ìš”ì²ì„ ë°›ì„ ìˆ˜ 있습니다.
+
+- ì»¤ë°‹ì€ ê°€ëŠ¥í•œ 한 작아야 하지만, ê° ì»¤ë°‹ì´ ë…립ì 으로 올바른지 확ì¸
+ 해야 합니다 (즉, ê° ì»¤ë°‹ì€ ì»´íŒŒì¼ë˜ì–´ 테스트를 통과해야 함).
+
+- 커밋ì—는 ê°œë°œìž ì¶œì²˜ ì¦ëª…서 (http://developercertificate.org)
+ ì„œëª…ì´ ì²¨ë¶€ë˜ì–´ì•¼ 하며, ì´ëŠ” 귀하 (ë° í•´ë‹¹ë˜ëŠ” 경우 ê³ ìš©ì£¼)ê°€
+ [프로ì 트 ë¼ì´ì„ 스](../LICENCE). ì¡°ê±´ì— êµ¬ì†ë˜ëŠ” ë° ë™ì˜í•œë‹¤ëŠ” ê²ƒì„ ë‚˜íƒ€ëƒ…ë‹ˆë‹¤.
+ gitì—서는 `git commit`ì— `-s` 옵션입니다
+
+- 패치가 ê²€í† ë˜ì§€ 않거나 íŠ¹ì •ì¸ì´ ê²€í† í•´ì•¼ 하는 경우, í’€ 리퀘스트나
+ 댓글ì—서 ê²€í† ìžì—게 @-ë‹µê¸€ì„ ë³´ë‚´ ê²€í† ë¥¼ ìš”ì²í•˜ê±°ë‚˜
+ [ì´ë©”ì¼](mailto:info@rustdesk.com)ì„ í†µí•´ ê²€í† ë¥¼ ìš”ì²í• 수 있습니다.
+
+- ìˆ˜ì •ëœ ë²„ê·¸ ë˜ëŠ” 새 기능과 ê´€ë ¨ëœ í…ŒìŠ¤íŠ¸ë¥¼ 추가합니다.
+
+구체ì ì¸ git 지침ì€, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)ì„ ì°¸ì¡°í•˜ì„¸ìš”.
+
+## í–‰ë™ ê°•ë ¹
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
+
+## 커뮤니케ì´ì…˜
+
+RustDesk 기여ìžë“¤ì€ [Discord](https://discord.gg/nDceKgxnkV)ì—서 활ë™í•˜ê³ 있습니다.
diff --git a/docs/CONTRIBUTING-NO.md b/docs/CONTRIBUTING-NO.md
new file mode 100644
index 00000000000..89a57456327
--- /dev/null
+++ b/docs/CONTRIBUTING-NO.md
@@ -0,0 +1,46 @@
+# Bidrag til RustDesk
+
+RustDesk er åpene for bidrag fra alle. Her er reglene for de som har lyst til å
+hjelpe oss:
+
+## Bidrag
+
+Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests.
+Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til
+Ã¥ godkjenne endringene) og enten bli sendt til main treet eller respondert med
+tilbakemelding på endringer som er nødvendig. Alle bidrag burde følge dette formate
+også de fra kjerne bidragere.
+
+Om du ønsker å jobbe på en issue må du huske å gjøre krav på den først. Dette
+kann gjøres ved å kommentere på den GitHub issue-en du ønsker å jobbe på.
+Dette er for å hindre duplikat innsats på samme problem.
+
+## Pull Request Sjekkliste
+
+- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nåværende
+ master grenen før du sender inn din pull request. Hvis ikke dette gjøres på rent
+ vis vill du bli spurt om å rebase dine endringer.
+
+- Commits burde være så små som mulig, samtidig som de må være korrekt uavhenging av hverandre
+ (hver commit burde kompilere og bestå tester).
+
+- Commits burde være akkopaniert med en Developer Certificate of Origin
+ (http://developercertificate.org), som indikerer att du (og din arbeidsgiver
+ i det tilfellet) godkjenner å bli knyttet til vilkårene av [prosjekt lisensen](../LICENCE).
+ Ved bruk av git er dette `-s` opsjonen til `git commit`.
+
+- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til
+ å se på dem kan du @-svare en med autoritet til å godkjenne dine endringer.
+ Dette kann gjøres i en pull request, en kommentar eller via epost på [email](mailto:info@rustdesk.com).
+
+- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet.
+
+For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## Oppførsel
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
+
+## Kommunikasjon
+
+RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING-RU.md b/docs/CONTRIBUTING-RU.md
index acc233d003f..1cf9a472da7 100644
--- a/docs/CONTRIBUTING-RU.md
+++ b/docs/CONTRIBUTING-RU.md
@@ -5,18 +5,14 @@ RustDesk приветÑтвует вклад каждого.
## Вклад в развитие
-Вклады в развитие RustDesk или его завиÑимоÑти должны быть
-Ñделаны в виде `pull request` на GitHub. Каждый такой
-`pull request` будет раÑÑмотрен оÑновным учаÑтником
-(кем-то, у кого еÑть разрешение на влив иÑправлений)
-и либо помещен в оÑновное дерево, либо Вам будет дан отзыв
-о необходимых правках. Ð’Ñе материалы должны ÑоответÑтвовать
-Ñтому формату, даже те, которые поÑтупают от оÑновных авторов.
-
-ЕÑли вы хотите поработать над какой-либо проблемой, то пожалуйÑта,
-Ñначала напишите об Ñтом, Ñоздав тикет на GitHub, и опиÑав,
-над чем вы хотите поработать. Ðто делаетÑÑ Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы
-предотвратить дублирование уÑилий учаÑтников по одному и тому же вопроÑу.
+Вклады в развитие RustDesk или его завиÑимоÑти должны быть Ñделаны в виде `pull request` на GitHub.
+Каждый такой `pull request` будет раÑÑмотрен оÑновным учаÑтником (кем-то, у кого еÑть разрешение
+на влив иÑправлений) и либо помещен в оÑновное дерево, либо Вам будет дан отзыв о необходимых правках.
+Ð’Ñе материалы должны ÑоответÑтвовать Ñтому формату, даже те, которые поÑтупают от оÑновных авторов.
+
+ЕÑли вы хотите поработать над какой-либо проблемой, то пожалуйÑта, Ñначала напишите об Ñтом,
+Ñоздав `issue` на GitHub, и опиÑав, над чем вы хотите поработать. Ðто делаетÑÑ Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾,
+чтобы предотвратить дублирование уÑилий учаÑтников по одному и тому же вопроÑу.
## Контрольный ÑпиÑок Ð´Ð»Ñ Ð’Ð°ÑˆÐ¸Ñ… `pull request`
@@ -24,13 +20,13 @@ RustDesk приветÑтвует вклад каждого.
ветку перед отправкой `pull request`. При наличии конфликтов ÑлиÑÐ½Ð¸Ñ Ð²Ð°Ð¼ будет
предложено их уÑтранить, возможно при помощи того же `rebase`.
-- Коммиты должны быть, по возможноÑти, небольшим, при Ñтом гарантируÑ, что каждаый
+- Коммиты должны быть, по возможноÑти, небольшими, при Ñтом гарантируÑ, что каждый
коммит ÑвлÑетÑÑ Ð½ÐµÐ·Ð°Ð²Ð¸Ñимо правильным (Ñ‚.е., каждый коммит должен компилироватьÑÑ Ð¸ проходить теÑты).
-- Коммиты должны ÑопровождатьÑÑ `Developer Certificate of Origin`
- (http://developercertificate.org) подпиÑью, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑƒÐºÐ°Ð¶ÐµÑ‚ на то, что вы (и
- ваш работодатель, еÑли Ñто применимо) ÑоглаÑны Ñоблюдать уÑловиÑ
- [лицензии проекта](../LICENCE). Ð’ `git` Ñто флаг `-s` при иÑпользовании `git commit`
+- Коммиты должны ÑопровождатьÑÑ Ð¿Ð¾Ð´Ð¿Ð¸Ñью `Developer Certificate of Origin`
+ (http://developercertificate.org), ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑƒÐºÐ°Ð¶ÐµÑ‚ на то, что вы (и ваш работодатель,
+ еÑли Ñто применимо) ÑоглаÑны Ñоблюдать уÑÐ»Ð¾Ð²Ð¸Ñ [лицензии проекта](../LICENCE).
+ Ð’ `git` Ñто флаг `-s` при иÑпользовании `git commit`
- ЕÑли ваш патч не проходит рецензирование или вам нужно,
чтобы его проверил конкретный человек, Вы можете ответить рецензенту через `@`,
@@ -40,7 +36,7 @@ RustDesk приветÑтвует вклад каждого.
Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ñ‹Ñ… инÑтрукций `git` Ñм. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow).
-## ÐšÐ¾Ð´ÐµÐºÑ Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков
+## Правила Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков
Ðормы Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð²Ð½ÑƒÑ‚Ñ€Ð¸ ÑообщеÑтва подробно опиÑаны [здеÑÑŒ](CODE_OF_CONDUCT-RU.md).
diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md
deleted file mode 100644
index 2a0d73f1797..00000000000
--- a/docs/DEVCONTAINER-DE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binärprogramm im Debug-Modus erstellt.
-
-Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an.
-
-Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgeführt werden müssen, um bestimmte Builds zu erstellen.
-
-Kommando|Build-Typ|Modus
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|release
-
diff --git a/docs/DEVCONTAINER-IT.md b/docs/DEVCONTAINER-IT.md
deleted file mode 100644
index 713c6fc3768..00000000000
--- a/docs/DEVCONTAINER-IT.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug.
-
-Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio.
-
-Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche.
-
-Comando|Tipo build|Modo
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|release
-
diff --git a/docs/DEVCONTAINER-JP.md b/docs/DEVCONTAINER-JP.md
deleted file mode 100644
index d8a599bef8c..00000000000
--- a/docs/DEVCONTAINER-JP.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-docker コンテナ㧠devcontainer ã‚’èµ·å‹•ã™ã‚‹ã¨ã€ãƒ‡ãƒãƒƒã‚°ãƒ¢ãƒ¼ãƒ‰ã® linux ãƒã‚¤ãƒŠãƒªãŒä½œæˆã•れã¾ã™ã€‚
-
-ç¾åœ¨ devcontainer ã§ã¯ã€Linux 㨠android ã®ãƒ“ルドをデãƒãƒƒã‚°ãƒ¢ãƒ¼ãƒ‰ã¨ãƒªãƒªãƒ¼ã‚¹ãƒ¢ãƒ¼ãƒ‰ã®ä¸¡æ–¹ã§æä¾›ã—ã¦ã„ã¾ã™ã€‚
-
-以下ã¯ã€ç‰¹å®šã®ãƒ“ルドを作æˆã™ã‚‹ãŸã‚ã«ãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆã®ãƒ«ãƒ¼ãƒˆã‹ã‚‰å®Ÿè¡Œã™ã‚‹ã‚³ãƒžãƒ³ãƒ‰ã®è¡¨ã«ãªã‚Šã¾ã™ã€‚
-
-コマンド|ビルド タイプ|モード
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|release
-
diff --git a/docs/DEVCONTAINER-NL.md b/docs/DEVCONTAINER-NL.md
deleted file mode 100644
index cd6ae456d89..00000000000
--- a/docs/DEVCONTAINER-NL.md
+++ /dev/null
@@ -1,15 +0,0 @@
-
-Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt.
-
-Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus.
-
-Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden
-uitgevoerd om specifieke builds te maken.
-
-Commando|Build Type|Modus
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|debug
-
diff --git a/docs/DEVCONTAINER-PL.md b/docs/DEVCONTAINER-PL.md
deleted file mode 100644
index 0aae2b975e3..00000000000
--- a/docs/DEVCONTAINER-PL.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania.
-
-Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej.
-
-Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji.
-
-Polecenie|Typ kompilacji|Tryb
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|debug
-
diff --git a/docs/DEVCONTAINER-TR.md b/docs/DEVCONTAINER-TR.md
deleted file mode 100644
index 7fc14ce5ee9..00000000000
--- a/docs/DEVCONTAINER-TR.md
+++ /dev/null
@@ -1,12 +0,0 @@
-Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
-
-Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
-
-Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
-
-Komut | Derleme Türü | Mod
--|-|-
-`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
-`.devcontainer/build.sh --release linux` | Linux | sürüm
-`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
-`.devcontainer/build.sh --release android` | Android-arm64 | sürüm
diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md
deleted file mode 100644
index 3d04fd3994e..00000000000
--- a/docs/DEVCONTAINER.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-After the start of devcontainer in docker container, a linux binary in debug mode is created.
-
-Currently devcontainer offers linux and android builds in both debug and release mode.
-
-Below is the table on commands to run from root of the project for creating specific builds.
-
-Command|Build Type|Mode
--|-|-|
-`.devcontainer/build.sh --debug linux`|Linux|debug
-`.devcontainer/build.sh --release linux`|Linux|release
-`.devcontainer/build.sh --debug android`|android-arm64|debug
-`.devcontainer/build.sh --release android`|android-arm64|release
-
diff --git a/docs/README-AR.md b/docs/README-AR.md
index d6800dc6fc2..832ed5e8344 100644
--- a/docs/README-AR.md
+++ b/docs/README-AR.md
@@ -9,7 +9,7 @@
لغتك الأم, Doc Ùˆ RustDesk UI, README Ù†ØÙ† Ø¨ØØ§Ø¬Ø© إلى مساعدتك لترجمة هذا
-[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) :تواصل معنا عبر
+[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) :تواصل معنا عبر
[](https://ko-fi.com/I2I04VU09)
@@ -27,6 +27,7 @@
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
+
## التبعيات
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة Ø³Ø·Ø Ø§Ù„Ù…ÙƒØªØ¨ تستخدم
diff --git a/docs/README-CS.md b/docs/README-CS.md
index 2a89997ce49..a00aa1a588f 100644
--- a/docs/README-CS.md
+++ b/docs/README-CS.md
@@ -9,7 +9,7 @@
Potřebujeme Vaši pomoc s překladem tohoto README, uživatelského rozhranà aplikace RustDesk a dokumentace k nà do vašeho jazyka
-PopovÃdejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+PopovÃdejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-DA.md b/docs/README-DA.md
index 5ea61590963..2c6987053f3 100644
--- a/docs/README-DA.md
+++ b/docs/README-DA.md
@@ -9,13 +9,13 @@
Vi har brug for din hjælp til at oversætte denne README, RustDesk UI og Dokument til dit modersmål
-Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo).
-RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang.
+RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang.
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
diff --git a/docs/README-DE.md b/docs/README-DE.md
index 28e0bc19a1f..a4a5453c01d 100644
--- a/docs/README-DE.md
+++ b/docs/README-DE.md
@@ -9,7 +9,12 @@
Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen.
-Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!Vorsicht]
+> **Haftungsausschluss bei Missbrauch::**
+> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung.
+
+
+Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-EO.md b/docs/README-EO.md
index de44d7a1600..e6bbd3ddec1 100644
--- a/docs/README-EO.md
+++ b/docs/README-EO.md
@@ -9,7 +9,7 @@
Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo
-Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-ES.md b/docs/README-ES.md
index dea12c15253..607295bea02 100644
--- a/docs/README-ES.md
+++ b/docs/README-ES.md
@@ -9,12 +9,18 @@
Necesitamos tu ayuda para traducir este README a tu idioma
-Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!Caution]
+> **Descargo de responsabilidad por mal uso:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación.
+
+Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
+
+
RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar.
[**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
@@ -24,12 +30,15 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`
[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](https://flathub.org/apps/com.rustdesk.RustDesk)
## Dependencias
-La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter.
+Las versiones de escritorio utilizan Flutter o Sciter (obsoleto) para GUI, este tutorial es sólo para Sciter, ya que es más fácil y más amigable para empezar. Echa un vistazo a nuestro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para la construcción de la versión Flutter.
-Por favor descarga la librerÃa dinámica de Sciter tu mismo.
+Por favor descarga la librerÃa dinámica de Sciter tú mismo.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -51,13 +60,21 @@ Por favor descarga la librerÃa dinámica de Sciter tu mismo.
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
-sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -96,12 +113,12 @@ cd
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
-git clone https://github.com/rustdesk/rustdesk
+git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Como compilar con Docker
@@ -111,10 +128,11 @@ Empieza clonando el repositorio y compilando el contenedor de docker:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
+git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
-Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando:
+Entonces, cada vez que necesites compilar la aplicación, ejecuta el siguiente comando:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
@@ -147,12 +165,16 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raÃz del
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
+> [!Precaución]
+> **Descargo de responsabilidad por uso indebido:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación.
+
## Capturas de pantalla
-
+
-
+
-
+
-
+
diff --git a/docs/README-FA.md b/docs/README-FA.md
index ca060011b29..704775ffcaa 100644
--- a/docs/README-FA.md
+++ b/docs/README-FA.md
@@ -9,7 +9,7 @@
[English] | [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Ελληνικά]
برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.
-با ما Ú¯ÙØªÚ¯Ùˆ کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
+با ما Ú¯ÙØªÚ¯Ùˆ کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-FI.md b/docs/README-FI.md
index 2ba44394fcd..b0956c212ca 100644
--- a/docs/README-FI.md
+++ b/docs/README-FI.md
@@ -9,7 +9,7 @@
Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi
-Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-FR.md b/docs/README-FR.md
index 1ad38ebc29f..39ff74f2004 100644
--- a/docs/README-FR.md
+++ b/docs/README-FR.md
@@ -9,7 +9,7 @@
Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle.
-Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -137,6 +137,10 @@ Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme
+> [!Attention]
+> **Avertissement contre l'utilisation abusive:**
+> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application.
+
## Images

diff --git a/docs/README-GR.md b/docs/README-GR.md
index 6fbb78fc84f..d834708ba6d 100644
--- a/docs/README-GR.md
+++ b/docs/README-GR.md
@@ -9,7 +9,7 @@
ΧÏειαζόμαστε τη βοήθειά σας για να μεταφÏάσουμε αυτό το αÏχείο README, το RustDesk UI και το Doc στη μητÏική σας γλώσσα
-Επικοινωνήστε μαζί μας μÎσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Επικοινωνήστε μαζί μας μÎσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -17,7 +17,7 @@

-Το RustDesk ενθαÏÏÏνει τη συνεισφοÏά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
+Το RustDesk ενθαÏÏÏνει τη συνεισφοÏά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
[**ΣυχνÎÏ‚ εÏωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
diff --git a/docs/README-HU.md b/docs/README-HU.md
index 07c0fdc1d89..c0275f7f11d 100644
--- a/docs/README-HU.md
+++ b/docs/README-HU.md
@@ -9,7 +9,7 @@
Kell a segÃtséged, hogy lefordÃtsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre
-Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-ID.md b/docs/README-ID.md
index ae4bbfb75f1..b953b604ec7 100644
--- a/docs/README-ID.md
+++ b/docs/README-ID.md
@@ -9,7 +9,7 @@
Kami membutuhkan bantuanmu untuk menterjemahkan file README dan RustDesk UI ke Bahasa Indonesia
-Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-IT.md b/docs/README-IT.md
index 4459f242352..7fac0e6e0ce 100644
--- a/docs/README-IT.md
+++ b/docs/README-IT.md
@@ -9,7 +9,7 @@
Abbiamo bisogno del tuo aiuto per tradurre questo file README e la UI RustDesk nella tua lingua nativa
-Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -164,6 +164,10 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
+> [!Attenzione]
+> **Dichiarazione di non responsabilità per uso improprio:**
+> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione.
+
## Schermate

diff --git a/docs/README-JP.md b/docs/README-JP.md
index f1245fa05af..9beb259f2bb 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -5,11 +5,11 @@
Docker •
Structure •
Snapshot
- [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
READMEã‚„RustDesk UI〠RustDesk Docã®ç¿»è¨³è€…ã‚’æ“迎ã—ã¾ã™ï¼
-ç§ãŸã¡ã¨è©±ã™: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+ç§ãŸã¡ã¨è©±ã™: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Rustã§æ›¸ã‹ã‚ŒãŸã€è¨å®šä¸è¦ã§ã™ãã«ä½¿ãˆã‚‹ãƒªãƒ¢ãƒ¼ãƒˆãƒ‡ã‚¹ã‚¯ãƒˆ

RustDeskã¯çš†ã•ã‚“ã®è²¢çŒ®ã‚’æ“迎ã—ã¾ã™ã€‚
-è²¢çŒ®ã®æ–¹æ³•ã«ã¤ã„ã¦ã¯[CONTRIBUTING.md](docs/CONTRIBUTING.md)ã‚’ã”確èªãã ã•ã„。
+è²¢çŒ®ã®æ–¹æ³•ã«ã¤ã„ã¦ã¯[CONTRIBUTING.md](CONTRIBUTING.md)ã‚’ã”確èªãã ã•ã„。
[**よãã‚る質å•**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -168,6 +168,10 @@ target/release/rustdesk
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップã¨ãƒ¢ãƒã‚¤ãƒ«å‘ã‘ã®Flutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアントå‘ã‘ã®JavaScript
+> [!注æ„]
+> **:䏿£ä½¿ç”¨ã«é–¢ã™ã‚‹å…è²¬äº‹é …**
+> RustDeskã®é–‹ç™ºè€…ã¯ã€ã“ã®ã‚½ãƒ•トウェアã®éžå€«ç†çš„ã¾ãŸã¯é•法ãªä½¿ç”¨ã‚’容èªã¾ãŸã¯æ”¯æŒã—ã¾ã›ã‚“ã€‚ä¸æ£ã‚¢ã‚¯ã‚»ã‚¹ã€ä¸æ£ãªåˆ¶å¾¡ã€ã¾ãŸã¯ãƒ—ライãƒã‚·ãƒ¼ã®ä¾µå®³ãªã©ã®ä¸æ£ä½¿ç”¨ã¯ã€å½“社ã®ã‚¬ã‚¤ãƒ‰ãƒ©ã‚¤ãƒ³ã«å޳坆ã«é•åã—ã¾ã™ã€‚開発者ã¯ã€ã‚¢ãƒ—リケーションã®ä¸æ£ä½¿ç”¨ã«å¯¾ã—ã¦ä¸€åˆ‡ã®è²¬ä»»ã‚’è² ã„ã¾ã›ã‚“。
+
## スクリーンショット

diff --git a/docs/README-KR.md b/docs/README-KR.md
index b0a8b973e2a..d2123982225 100644
--- a/docs/README-KR.md
+++ b/docs/README-KR.md
@@ -1,64 +1,84 @@

- Servers •
- Build •
+ 빌드 •
Docker •
- Structure •
- Snapshot
- [English] | [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [العربي] | [Tiếng Việt] | [Ελληνικά]
- README를 모êµì–´ë¡œ 번ì—하기 위한 ë‹¹ì‹ ì˜ ë„ì›€ì˜ í•„ìš”í•©ë‹ˆë‹¤.
+ 구조 •
+ 스냇샷
+ [English] | [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ ì´ README, RustDesk UI ë° RustDesk 문서를 ê·€í•˜ì˜ ëª¨êµì–´ë¡œ 번ì—하는 ë° ë„ì›€ì´ í•„ìš”í•©ë‹ˆë‹¤
-채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!Caution]
+> **오용 ë©´ì±… ì¡°í•:**
+> RustDeskì˜ ê°œë°œìžëŠ” ì´ ì†Œí”„íŠ¸ì›¨ì–´ì˜ ë¹„ìœ¤ë¦¬ì ë˜ëŠ” 불법ì ì¸ ì‚¬ìš©ì„ ë¬µì¸í•˜ê±°ë‚˜ ì§€ì›í•˜ì§€ 않습니다. 무단 액세스, ì œì–´ ë˜ëŠ” ê°œì¸ì •ë³´ 침해와 ê°™ì€ ì˜¤ìš©ì€ ì—„ê²©í•˜ê²Œ ë‹¹ì‚¬ì˜ ì§€ì¹¨ì— ìœ„ë°°ë©ë‹ˆë‹¤. 작성ìžëŠ” ì‘ìš© í”„ë¡œê·¸ëž¨ì˜ ì˜¤ìš©ì— ëŒ€í•´ ì±…ìž„ì„ ì§€ì§€ 않습니다.
+우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
+
[](https://ko-fi.com/I2I04VU09)
-Rust로 작성ë˜ì—ˆê³ , ì„¤ì •ì—†ì´ ë°”ë¡œ ì‚¬ìš©í• ìˆ˜ 있는 ì›ê²© ë°ìŠ¤íŠ¸íƒ‘ 소프트웨어입니다. ìžì‹ ì˜ ë°ì´í„°ë¥¼ ì™„ì „ížˆ ì»¨íŠ¸ë¡¤í• ìˆ˜ ìžˆê³ , ë³´ì•ˆì˜ ì—¼ë ¤ë„ ì—†ìŠµë‹ˆë‹¤. ìš°ë¦¬ì˜ rendezvous/relay 서버를 사용해ë„, [ì§ì ‘ ì„¤ì •](https://rustdesk.com/server)하거나 [ì§ì ‘ rendezvous/relay 서버를 ìž‘ì„±í• ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤](https://github.com/rustdesk/rustdesk-server-demo).
+Rust로 ìž‘ì„±ëœ ë˜ ë‹¤ë¥¸ ì›ê²© ë°ìФí¬í†± 소프트웨어입니다. êµ¬ì„±í• í•„ìš” ì—†ì´ ë°”ë¡œ ì‚¬ìš©í• ìˆ˜ 있습니다. ë³´ì•ˆì— ëŒ€í•œ ê±±ì • ì—†ì´ ë°ì´í„°ë¥¼ 완벽하게 ì œì–´í• ìˆ˜ 있습니다. ì €í¬ì˜ rendezvous/relay server 서버를 사용하거나, [ì§ì ‘ ì„¤ì •](https://rustdesk.com/server), ë˜ëŠ” [ì§ì ‘ rendezvous/relay 서버를 ìž‘ì„±í• ìˆ˜ 있습니다](https://github.com/rustdesk/rustdesk-server-demo).

-RustDesk는 ëª¨ë“ ê¸°ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. ê¸°ì—¬í•˜ê³ ìž í•œë‹¤ë©´ [`docs/CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요.
+RustDesk는 ëª¨ë“ ë¶„ë“¤ì˜ ê¸°ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. 시작하는 ë° ë„ì›€ì´ í•„ìš”í•˜ë©´ [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요.
+
+[**ìžì£¼ 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**ë°”ì´ë„ˆë¦¬ 다운로드**](https://github.com/rustdesk/rustdesk/releases)
-[**RustDesk는 어떻게 ìž‘ë™í•˜ëŠ”ê°€?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+[**ê°œë°œìž ë¹Œë“œ**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
-[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](https://flathub.org/apps/com.rustdesk.RustDesk)
-## ì˜ì¡´ê´€ê³„
+## 종ì†ì„±
-ë°ìФí¬íƒ‘íŒì—는 GUIì— [sciter](https://sciter.com/)ê°€ 사용ë˜ì—ˆìŠµë‹ˆë‹¤. sciter dynamic library 를 다운로드해주세요.
+ë°ìФí¬í†± ë²„ì „ì€ GUI로 Flutter ë˜ëŠ” Sciter (ë” ì´ìƒ ì§€ì›ë˜ì§€ 않ìŒ)를 사용하며, ì´ ìžìŠµì„œëŠ” 시작하기 ë” ì‰½ê³ ì¹œìˆ™í•œ Sciter ì „ìš©ìž…ë‹ˆë‹¤. Flutter ë²„ì „ 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)ì„ í™•ì¸í•˜ì„¸ìš”.
+
+Sciter ë™ì ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ ì§ì ‘ 다운로드하세요.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
-[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-
-ëª¨ë°”ì¼ ë²„ì „ì€ Flutter를 사용합니다. ë°ìФí¬íƒ‘ ë˜í•œ Sciterì—서 Flutter로 마ì´ê·¸ë ˆì´ì…˜í• ì˜ˆì •ìž…ë‹ˆë‹¤.
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-## 빌드 순서
+## 빌드를 위한 ì›ì‹œ 단계
-- Rust 개발환경, C++ 빌드 í™˜ê²½ì„ ì¤€ë¹„í•©ë‹ˆë‹¤.
+- Rust 개발 환경과 C++ 빌드 í™˜ê²½ì„ ì¤€ë¹„í•©ë‹ˆë‹¤
-- [vcpkg](https://github.com/microsoft/vcpkg) ì„¤ì¹˜í•˜ê³ `VCPKG_ROOT` 환경변수를 ì •í™•ížˆ ì„¤ì •í•©ë‹ˆë‹¤.
+- [vcpkg](https://github.com/microsoft/vcpkg)를 ì„¤ì¹˜í•˜ê³ `VCPKG_ROOT` 환경 변수를 올바르게 ì„¤ì •í•©ë‹ˆë‹¤
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- - Linux/MacOS: vcpkg install libvpx libyuv opus aom
+ - Linux/macOS: vcpkg install libvpx libyuv opus aom
-- 실행 `cargo run`
+- `cargo run` 실행
## [빌드](https://rustdesk.com/docs/en/dev/build/)
-## Linuxì—서 빌드 순서
+## Linuxì—서 빌드하는 방법
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
-sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -79,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
-### libvpx ìˆ˜ì • (For Fedoraìš©)
+### libvpx ìˆ˜ì • (Fedoraìš©)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -97,7 +117,7 @@ cd
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
-git clone https://github.com/rustdesk/rustdesk
+git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -105,56 +125,58 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
-## Dockerì— ë¹Œë“œí•˜ëŠ” 방법
+## Docker로 빌드하는 방법
-리í¬ì§€í† 리를 í´ë¡ í•˜ê³ , Docker 컨테ì´ë„ˆ 구성하는 것으로 시작합니다.
+ë¨¼ì € 리í¬ì§€í† 리를 ë³µì œí•˜ê³ Docker 컨테ì´ë„ˆë¥¼ 빌드합니다:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
+git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
-ì´í›„, ì• í”Œë¦¬ì¼€ì´ì…˜ì„ ë¹Œë“œí• í•„ìš”ê°€ ìžˆì„ ë•Œë§ˆë‹¤, 아래ì˜ì˜ ëª…ë ¹ì„ ì‹¤í–‰í•©ë‹ˆë‹¤.
+그런 ë‹¤ìŒ ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ë¹Œë“œí•´ì•¼ í• ë•Œë§ˆë‹¤ ë‹¤ìŒ ëª…ë ¹ì„ ì‹¤í–‰í•©ë‹ˆë‹¤:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
-첫 빌드ì—서는 ì˜ì¡´ê´€ê³„ê°€ ìºì‹œë 때까지 ì‹œê°„ì´ ê±¸ë¦´ 수 있습니다만, ì´í›„ì˜ ë¹Œë“œë•ŒëŠ” 빨ë¼ì§‘니다. ë”불어 빌드 ëª…ë ¹ì— ë‹¤ë¥¸ ì¸ìˆ˜ë¥¼ ì§€ì •í• í•„ìš”ê°€ 있다면, ëª…ë ¹ ëì— ìžˆëŠ” `` ì— ì§€ì •í• ìˆ˜ 있습니다. 예를 들어 최ì í™”ëœ ì¶œì‹œ ë²„ì „ì„ ë¹Œë“œí•˜ê³ ì‹¶ë‹¤ë©´ ì´ë ‡ê²Œ ìƒê¸°í•œ ëª…ë ¹ ë’¤ì— `--release` 를 붙여 실행합니다. 성공했다면 실행파ì¼ì€ 시스템 타겟 í´ë”ì— ë‹´ê²¨ì§€ê³ , ë‹¤ìŒ ëª…ë ¹ìœ¼ë¡œ ì‹¤í–‰í• ìˆ˜ 있습니다.
+첫 번째 빌드는 종ì†ì„±ì´ ìºì‹œë˜ê¸°ê¹Œì§€ ì‹œê°„ì´ ì˜¤ëž˜ 걸릴 수 있으며, ì´í›„ 빌드는 ë” ë¹¨ë¼ì§‘니다. ë˜í•œ 빌드 ëª…ë ¹ì— ë‹¤ë¥¸ ì¸ìˆ˜ë¥¼ ì§€ì •í•´ì•¼ 하는 경우 ëª…ë ¹ ëì˜ `` ìœ„ì¹˜ì— ì¸ìˆ˜ë¥¼ ì§€ì •í• ìˆ˜ 있습니다. 예를 들어 최ì í™”ëœ ë¦´ë¦¬ìŠ¤ ë²„ì „ì„ ë¹Œë“œí•˜ë ¤ë©´ ìœ„ì˜ ëª…ë ¹ ë’¤ì— `--release`를 추가하면 ë©ë‹ˆë‹¤. ê²°ê³¼ 실행 파ì¼ì€ ì‹œìŠ¤í…œì˜ ëŒ€ìƒ í´ë”ì—서 ì‚¬ìš©í• ìˆ˜ 있으며 ì‹¤í–‰í• ìˆ˜ 있습니다::
```sh
target/debug/rustdesk
```
-í˜¹ì€ ì¶œì‹œìš© 실행 파ì¼ì„ ì‹¤í–‰í• ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤.
+ë˜ëŠ” 릴리스 실행 파ì¼ì„ 실행하는 경우:
```sh
target/release/rustdesk
```
-ëª…ë ¹ì„ RustDesk 리í¬ì§€í† 리 루트ì—서 실행한다는 ê²ƒì„ í™•ì¸í•´ì£¼ì„¸ìš”. ê·¸ë ‡ê²Œ 하지 않으면 ì• í”Œë¦¬ì¼€ì´ì…˜ì´ 필요한 리소스를 발견하지 못 í• ê°€ëŠ¥ì„±ì´ ìžˆìŠµë‹ˆë‹¤. ë˜í•œ `install`, `run` ê°™ì€ cargo 하위 ëª…ë ¹ì€ í˜¸ìŠ¤íŠ¸ê°€ ì•„ë‹ˆë¼ ì»¨í…Œì´ë„ˆ í”„ë¡œê·¸ëž¨ì„ ì„¤ì¹˜, ì‹¤í–‰ì„ ìœ„í•¨ì´ë¯€ë¡œ 현재 ì´ ë°©ë²•ì€ ì§€ì›í•˜ì§€ 않다는 ì ì„ ìœ ë…해주시길 ë°”ëžë‹ˆë‹¤.
+RustDesk 리í¬ì§€í† ë¦¬ì˜ ë£¨íŠ¸ì—서 ì´ëŸ¬í•œ ëª…ë ¹ì„ ì‹¤í–‰í•˜ê³ ìžˆëŠ”ì§€ 확ì¸í•˜ì„¸ìš”. ê·¸ë ‡ì§€ 않으면 ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í•„ìš”í•œ 리소스를 찾지 ëª»í• ìˆ˜ 있습니다. ë˜í•œ `install` ë˜ëŠ” `run` ê³¼ ê°™ì€ ë‹¤ë¥¸ cargo 하위 ëª…ë ¹ì€ í˜¸ìŠ¤íŠ¸ê°€ 아닌 컨테ì´ë„ˆ ë‚´ë¶€ì— í”„ë¡œê·¸ëž¨ì„ ì„¤ì¹˜í•˜ê±°ë‚˜ 실행하므로 현재 ì´ ë°©ë²•ì„ í†µí•´ ì§€ì›ë˜ì§€ 않는다는 ì ì— ìœ ì˜í•˜ì„¸ìš”.
## íŒŒì¼ êµ¬ì¡°
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 ì½”ë±, ì„¤ì •, tcp/udp ëž©í¼, protobuf, íŒŒì¼ ì „ì†¡ì„ ìœ„í•œ fs 함수, ê·¸ 외 ìœ í‹¸ë¦¬í‹° 함수
-- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
-- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: í”Œëž«í¼ ê³ ìœ í‚¤ë³´ë“œ/마우스 컨트롤
-- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, í´ë¦½ë³´ë“œ, ìž…ë ¥, 비디오 서비스 ê·¸ë¦¬ê³ ë„¤íŠ¸ì›Œí¬ ì—°ê²°
-- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 ì ‘ì† ì‹œìž‘
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 í†µì‹ í•´ì„œ 리모트 다ì´ë ‰íЏ (TCP hole punching) í˜¹ì€ relayed ì ‘ì†
-- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: í”Œëž«í¼ ê³ ìœ ì˜ ì½”ë“œ
-- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바ì¼ìš© Flutter 코드
-- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 í´ë¼ì´ì–¸íŠ¸ìš© ìžë°”스í¬ë¦½íЏ
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 ì½”ë±, 구성, tcp/udp wrapper, protobuf, íŒŒì¼ ì „ì†¡ì„ ìœ„í•œ fs 함수 ë° ê¸°íƒ€ ìœ í‹¸ë¦¬í‹° 함수
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡ì³
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫í¼ë³„ 키보드/마우스 ì œì–´
+- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOSìš© íŒŒì¼ ë³µì‚¬ ë° ë¶™ì—¬ë„£ê¸° 구현
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: ë” ì´ìƒ 사용ë˜ì§€ 않는 Sciter UI (ì§€ì› ì¤‘ë‹¨)
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/í´ë¦½ë³´ë“œ/ìž…ë ¥/비디오 서비스 ë° ë„¤íŠ¸ì›Œí¬ ì—°ê²°
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 연결 시작
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 í†µì‹ , ì›ê²© 다ì´ë ‰íЏ (TCP 홀 펀ì¹) ë˜ëŠ” ë¦´ë ˆì´ ì—°ê²° 대기
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫í¼ë³„ 코드
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: ë°ìФí¬í†± ë° ëª¨ë°”ì¼ìš© Flutter 코드
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 í´ë¼ì´ì–¸íŠ¸ìš© JavaScript
-## 스냅샷
+## 스í¬ë¦°ìƒ·
-
+
-
+
-
+
-
+
diff --git a/docs/README-ML.md b/docs/README-ML.md
index 4e92bf73643..13b74ff0282 100644
--- a/docs/README-ML.md
+++ b/docs/README-ML.md
@@ -9,7 +9,7 @@
à´ˆ README നിങàµà´™à´³àµà´Ÿàµ† മാതൃà´à´¾à´·à´¯à´¿à´²àµ‡à´•àµà´•ൠവിവർതàµà´¤à´¨à´‚ ചെയàµà´¯à´¾àµ» à´žà´™àµà´™àµ¾à´•àµà´•ൠനിങàµà´™à´³àµà´Ÿàµ† സഹായം ആവശàµà´¯à´®à´¾à´£àµ
-à´žà´™àµà´™à´³àµà´®à´¾à´¯à´¿ ചാറàµà´±àµ ചെയàµà´¯àµà´•: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+à´žà´™àµà´™à´³àµà´®à´¾à´¯à´¿ ചാറàµà´±àµ ചെയàµà´¯àµà´•: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-NL.md b/docs/README-NL.md
index 44fedd5afc4..cb5f5b3434a 100644
--- a/docs/README-NL.md
+++ b/docs/README-NL.md
@@ -9,7 +9,7 @@
Wij hebben uw hulp nodig om dit README bestand te vertalen, RustDesk UI en Doc naar uw moedertaal
-Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-NO.md b/docs/README-NO.md
new file mode 100644
index 00000000000..e77dcf8539b
--- /dev/null
+++ b/docs/README-NO.md
@@ -0,0 +1,177 @@
+
+ 
+ Servere •
+ Build •
+ Docker •
+ Struktur •
+ Snapshot
+ [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
+ Vi trenger din hjelp til å oversette denne README-en, RustDesk UI og RustDesk Doc tid ditt morsmål
+
+
+Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
+
+[](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).
+
+
+
+RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart.
+
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](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
+
+
+
+
+
+
+
+
+
diff --git a/docs/README-PL.md b/docs/README-PL.md
index 295564457b8..a68d87dfc24 100644
--- a/docs/README-PL.md
+++ b/docs/README-PL.md
@@ -9,7 +9,7 @@
Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język
-Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -165,6 +165,3 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru

-## [Serwery publiczne](#public-servers)
-
-RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md
index 64c5ae001de..ff1e8f7ef89 100644
--- a/docs/README-PTBR.md
+++ b/docs/README-PTBR.md
@@ -9,7 +9,7 @@
Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua lÃngua nativa
-Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -137,6 +137,10 @@ Por favor verifique que está executando estes comandos da raiz do repositório
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código especÃfico a cada plataforma
+> [!Cuidadob]
+> **Aviso de uso indevido:**
+> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
+
## Screenshots

diff --git a/docs/README-RU.md b/docs/README-RU.md
index 60972efd355..d6aaaf2cb79 100644
--- a/docs/README-RU.md
+++ b/docs/README-RU.md
@@ -1,42 +1,52 @@

- Servers •
- Build •
- Docker •
- Structure •
- Snapshot
+ Первичные шаги Ð´Ð»Ñ Ñборки •
+ Как Ñобрать Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Docker •
+ Структура файлов •
+ Скриншоты
[English] | [УкраїнÑька] | [Äesky] | [䏿–‡] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Ελληνικά]
- Ðам нужна ваша помощь Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ð¾Ð´Ð° Ñтого README RustDesk UI
- и документацию RustDesk на ваш родной Ñзык. RustDesk Doc
+ Ðам нужна ваша помощь в переводе Ñтого README, интерфейÑа RustDesk
+ и документации RustDesk на ваш родной Ñзык.
-Общение Ñ Ð½Ð°Ð¼Ð¸: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!Caution]
+> **Отказ от ответÑтвенноÑти за неправомерное иÑпользование**
+> Разработчики RustDesk не одобрÑÑŽÑ‚ и не поддерживают какое-либо неÑтичное или незаконное иÑпользование данного программного обеÑпечениÑ. Ðеправомерное иÑпользование (неÑанкционированный доÑтуп, контроль или вторжение в чаÑтную жизнь) Ñтрого противоречит нашим правилам. Ðвторы не неÑут ответÑтвенноÑти за любое неправомерное иÑпользование приложениÑ.
+
+Общение Ñ Ð½Ð°Ð¼Ð¸: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
-Еще одно программное обеÑпечение Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ рабочего Ñтола, напиÑанное на Rust. Работает из коробки, не требует наÑтройки. Ð’Ñ‹ полноÑтью контролируете Ñвои данные, не беÑпокоÑÑÑŒ о безопаÑноÑти. Ð’Ñ‹ можете иÑпользовать наш Ñервер ретранÑлÑции, [наÑтроить Ñвой ÑобÑтвенный](https://rustdesk.com/server), или [напиÑать Ñвой](https://github.com/rustdesk/rustdesk-server-demo).
+Ещё одно программное обеÑпечение Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ рабочего Ñтола, напиÑанное на Rust. Работает из коробки, наÑтройки не требует. Ð’Ñ‹ полноÑтью контролируете Ñвои данные, не беÑпокоÑÑÑŒ о безопаÑноÑти. Ð’Ñ‹ можете иÑпользовать наш Ñервер ретранÑлÑции, [наÑтроить Ñвой ÑобÑтвенный](https://rustdesk.com/server), или [напиÑать Ñвой](https://github.com/rustdesk/rustdesk-server-demo).

RustDesk приветÑтвует вклад каждого. ОзнакомьтеÑÑŒ Ñ [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы Ð´Ð»Ñ Ð¿Ð¾Ð½Ð¸Ð¼Ð°Ð½Ð¸Ñ.
-[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) (Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð½Ð° английÑком Ñзыке)
+
+[**ЧаÑто задаваемые вопроÑÑ‹**](https://github.com/rustdesk/rustdesk/wiki/FAQ) (Страница на английÑком Ñзыке)
[**СКÐЧÐТЬ ПРИЛОЖЕÐИЕ**](https://github.com/rustdesk/rustdesk/releases)
-[**ночные Ñборки (актуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+[**ÐОЧÐЫЕ СБОРКИ (Ðктуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
-[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[
](https://flathub.org/apps/com.rustdesk.RustDesk)
## ЗавиÑимоÑти
-ÐаÑтольные верÑии иÑпользуют [sciter](https://sciter.com/) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ð¸Ñ‡ÐµÑкого интерфейÑа, загрузите динамичеÑкую библиотеку sciter ÑамоÑтоÑтельно.
+Ð”Ð»Ñ ÐŸÐš-верÑии иÑпользуютÑÑ Ð±Ð¸Ð±Ð»Ð¸Ð¾Ñ‚ÐµÐºÐ¸ Flutter или Sciter (уÑтаревшее) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ð¸Ñ‡ÐµÑкого интерфейÑа. Данное руководÑтво подразумевает работу Ñ Sciter, так как он более проÑтой в иÑпользовании и Ñ Ð½Ð¸Ð¼ легче начать работу. Ð’Ñ‹ можете также поÑмотреть на механизм нашего [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) Ð´Ð»Ñ Ñборок на Flutter.
+
+Загрузите динамичеÑкую библиотеку Flutter ÑамоÑтоÑтельно.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
-[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-
-Мобильные верÑии иÑпользуют Flutter. Ð’ будущем мы перенеÑем наÑтольную верÑию Ñо Sciter на Flutter.
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Первичные шаги Ð´Ð»Ñ Ñборки
@@ -45,22 +55,32 @@ RustDesk приветÑтвует вклад каждого. Ознакомьт
- УÑтановите [vcpkg](https://github.com/microsoft/vcpkg), и правильно уÑтановите переменную `VCPKG_ROOT`
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- - Linux/MacOS: vcpkg install libvpx libyuv opus aom
+ - Linux/macOS: vcpkg install libvpx libyuv opus aom
+
+- Выполните команду `cargo run`
-- ЗапуÑтите `cargo run`
+## [Сборка](https://rustdesk.com/docs/ru/dev/build/)
## Как Ñобрать на Linux
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
-sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -99,7 +119,7 @@ cd
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
-git clone https://github.com/rustdesk/rustdesk
+git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -114,16 +134,17 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
+git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
-Затем каждый раз, когда вам нужно Ñобрать приложение, запуÑкайте Ñледующую команду:
+Затем при каждой Ñборке Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½Ñйте Ñледующую команду:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
-Обратите внимание, что Ð¿ÐµÑ€Ð²Ð°Ñ Ñборка может занÑть больше времени, прежде чем завиÑимоÑти будут кÑшированы, но поÑледующие Ñборки будут выполнÑтьÑÑ Ð±Ñ‹Ñтрее. Кроме того, еÑли вам нужно указать другие аргументы Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ñ‹ Ñборки, вы можете Ñделать Ñто в конце команды в переменной ``. Ðапример, еÑли вы хотите Ñоздать оптимизированную верÑию, вы должны запуÑтить приведенную выше команду и в конце Ñтроки добавить `--release`. Полученный иÑполнÑемый файл будет доÑтупен в целевой папке вашей ÑиÑтемы и может быть запущен Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ:
+Обратите внимание, что Ð¿ÐµÑ€Ð²Ð°Ñ Ñборка может занÑть больше времени, прежде чем завиÑимоÑти будут кÑшированы, но поÑледующие Ñборки будут выполнÑтьÑÑ Ð±Ñ‹Ñтрее. Кроме того, еÑли вам нужно указать другие аргументы Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ñ‹ Ñборки, вы можете Ñделать Ñто в конце команды в переменной ``. Ðапример, еÑли вы хотите Ñоздать оптимизированную верÑию, вы должны выполнить приведенную выше команду и в конце Ñтроки добавить `--release`. Полученный иÑполнÑемый файл будет доÑтупен в целевой папке вашей ÑиÑтемы и может быть запущен Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ñледующей команды:
```sh
target/debug/rustdesk
@@ -135,25 +156,28 @@ target/debug/rustdesk
target/release/rustdesk
```
-ПожалуйÑта, убедитеÑÑŒ, что вы запуÑкаете Ñти команды из ÐºÐ¾Ñ€Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ RustDesk, иначе приложение не Ñможет найти необходимые реÑурÑÑ‹. Также обратите внимание, что другие cargo подкоманды, такие как `install` или `run`, в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаютÑÑ Ñтим методом, поÑкольку они будут уÑтанавливать или запуÑкать программу внутри контейнера, а не на хоÑте.
+ПожалуйÑта, убедитеÑÑŒ, что вы запуÑкаете Ñти команды из ÐºÐ¾Ñ€Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ RustDesk, иначе приложение не Ñможет найти необходимые реÑурÑÑ‹. Также обратите внимание, что другие подкоманды Cargo, такие как `install` или `run`, в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаютÑÑ Ñтим методом, поÑкольку они будут уÑтанавливать или запуÑкать программу внутри контейнера, а не на хоÑте.
## Структура файлов
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфиг, обертка tcp/udp, protobuf, функции fs Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸ файлов и некоторые другие Ñлужебные функции
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфигурациÑ, враппер TCP/UDP, protobuf, функции файловой ÑиÑтемы Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸ файлов и некоторые другие Ñлужебные функции
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват Ñкрана
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Ñпецифичное Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ управление клавиатурой/мышью
-- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графичеÑкий пользовательÑкий интерфейÑ
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервиÑÑ‹ аудио/буфера обмена/ввода/видео и Ñетевых подключений
+- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: функционал буфера обмена файлами Ð´Ð»Ñ Windows, Linux, и macOS
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графичеÑкий пользовательÑкий Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð½Ð° Sciter (уÑтаревшее)
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервиÑÑ‹ аудио, буфера обмена, ввода, видео и Ñетевых подключений
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое Ñоединение
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÑвÑжитеÑÑŒ Ñ [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитеÑÑŒ удаленного прÑмого (обход TCP NAT) или ретранÑлируемого ÑоединениÑ
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÑвÑзь Ñ [Ñервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прÑмого (через TCP hole punching) или ретранÑлируемого ÑоединениÑ
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Ñпецифичный Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ код
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter Ð´Ð»Ñ ÐŸÐš-верÑии и мобильных уÑтройÑтв
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript Ð´Ð»Ñ Web-клиента Flutter
## Скриншоты
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/docs/README-TR.md b/docs/README-TR.md
index 3b4b34edd7c..d9481b2c745 100644
--- a/docs/README-TR.md
+++ b/docs/README-TR.md
@@ -10,7 +10,7 @@
README, RustDesk UI ve RustDesk Belge'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var
-Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kul

-RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
+RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın.
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -166,6 +166,10 @@ Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan e
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
+> [!Dikkat]
+> **Yanlış Kullanım Uyarısı:**
+> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
+
## Ekran Görüntüleri

diff --git a/docs/README-UA.md b/docs/README-UA.md
index 8f226914d70..fb880749423 100644
--- a/docs/README-UA.md
+++ b/docs/README-UA.md
@@ -9,7 +9,7 @@
Ðам потрібна ваша допомога Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐºÐ»Ð°Ð´Ñƒ цього README, інтерфейÑу та документації RustDesk вашою рідною мовою
-Ð¡Ð¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Ð¡Ð¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -17,7 +17,7 @@

-RustDesk вітає внеÑок кожного. ОзнайомтеÑÑ Ð· [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
+RustDesk вітає внеÑок кожного. ОзнайомтеÑÑ Ð· [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -172,6 +172,3 @@ target/release/rustdesk

-## [Публічні Ñервери](#публічні-Ñервери)
-
-RustDesk підтримуєтьÑÑ Ð±ÐµÐ·ÐºÐ¾ÑˆÑ‚Ð¾Ð²Ð½Ð¸Ð¼ європейÑьким Ñервером, любʼÑзно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
diff --git a/docs/README-VN.md b/docs/README-VN.md
index 9c8ebcf237a..db83aff1cb1 100644
--- a/docs/README-VN.md
+++ b/docs/README-VN.md
@@ -11,7 +11,7 @@
Chúng tôi rất hoan nghênh sá»± há»— trợ cá»§a bạn trong việc dịch trang README, trang giao diện ngưá»i dùng cá»§a RustDesk - RustDesk UI và trang tà i liệu cá»§a RustDesk - RustDesk Doc sang Tiếng Việt
-Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/README-ZH.md b/docs/README-ZH.md
index 4920ade6d96..2899aa01bf7 100644
--- a/docs/README-ZH.md
+++ b/docs/README-ZH.md
@@ -8,7 +8,11 @@
[English] | [УкраїнÑька] | [Äesky] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한êµì–´] | [العربي] | [Tiếng Việt] | [Ελληνικά]
-与我们交æµ: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+> [!CAUTION]
+> **å…责声明:**
+> RustDesk 的开å‘人员ä¸çºµå®¹æˆ–支æŒä»»ä½•ä¸é“å¾·æˆ–éžæ³•çš„è½¯ä»¶ä½¿ç”¨è¡Œä¸ºã€‚æ»¥ç”¨è¡Œä¸ºï¼Œä¾‹å¦‚æœªç»æŽˆæƒçš„è®¿é—®ã€æŽ§åˆ¶æˆ–ä¾µçŠ¯éšç§ï¼Œä¸¥æ ¼è¿å我们的准则。作者对应用程åºçš„任何滥用行为概ä¸è´Ÿè´£ã€‚
+
+与我们交æµ: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[](https://ko-fi.com/I2I04VU09)
diff --git a/docs/SECURITY-KR.md b/docs/SECURITY-KR.md
new file mode 100644
index 00000000000..94ce8f2ba18
--- /dev/null
+++ b/docs/SECURITY-KR.md
@@ -0,0 +1,7 @@
+# 보안 ì •ì±…
+
+## 취약ì ë³´ê³
+
+ì €í¬ëŠ” 프로ì íŠ¸ì˜ ë³´ì•ˆì„ ë§¤ìš° 중요하게 ìƒê°í•©ë‹ˆë‹¤. ëª¨ë“ ì‚¬ìš©ìžê°€ 발견한 취약ì ì„ ì €í¬ì—게 ë³´ê³ í• ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. RustDesk 프로ì 트ì—서 보안 취약ì ì´ ë°œê²¬ë˜ë©´ info@rustdesk.com으로 ì´ë©”ì¼ì„ ë³´ë‚´ ì±…ìž„ê° ìžˆê²Œ ë³´ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤.
+
+현재로서는 버그 현ìƒê¸ˆ í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤. ì €í¬ëŠ” í° ë¬¸ì œë¥¼ 해결하기 위해 ë…¸ë ¥í•˜ëŠ” 소규모 팀입니다. ì „ì²´ 커뮤니티를 위한 ì•ˆì „í•œ ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ê³„ì† êµ¬ì¶•í• ìˆ˜ 있ë„ë¡ ì·¨ì•½ì ì„ ì±…ìž„ê° ìžˆê²Œ ì‹ ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤.
diff --git a/docs/SECURITY-NO.md b/docs/SECURITY-NO.md
new file mode 100644
index 00000000000..1f8dcb411bd
--- /dev/null
+++ b/docs/SECURITY-NO.md
@@ -0,0 +1,9 @@
+# Sikkerhets Rettningslinjer
+
+## Reportering av en SÃ¥rbarhet
+
+Vi verdsetter pris på sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til å rapportere sårbarheter de oppdager til oss.
+Om du finner en sikkerhets sårbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved å sende oss en email til info@rustdesk.com.
+
+På dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver å løse ett stort problem. Vi trenger att du raporterer alle sårbarhetene
+annsvarfult så vi kan fortsettte å bygge ett en sikker applikasjon for hele felleskapet.
diff --git a/examples/custom_plugin/Cargo.toml b/examples/custom_plugin/Cargo.toml
deleted file mode 100644
index b9ee06ae734..00000000000
--- a/examples/custom_plugin/Cargo.toml
+++ /dev/null
@@ -1,28 +0,0 @@
-[package]
-name = "custom_plugin"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[lib]
-name = "custom_plugin"
-path = "src/lib.rs"
-crate-type = ["cdylib"]
-
-
-[features]
-default = ["flutter"]
-flutter = []
-
-[dependencies]
-lazy_static = "1.4.0"
-rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]}
-
-[profile.release]
-lto = true
-codegen-units = 1
-panic = 'abort'
-strip = true
-#opt-level = 'z' # only have smaller size after strip
-rpath = true
\ No newline at end of file
diff --git a/examples/custom_plugin/src/lib.rs b/examples/custom_plugin/src/lib.rs
deleted file mode 100644
index 0b21f3fc854..00000000000
--- a/examples/custom_plugin/src/lib.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use librustdesk::api::RustDeskApiTable;
-/// This file demonstrates how to write a custom plugin for RustDesk.
-use std::ffi::{c_char, c_int, CString};
-
-lazy_static::lazy_static! {
- pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap();
- pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap();
- // Do your own logic based on the API provided by RustDesk.
- pub static ref API: RustDeskApiTable = RustDeskApiTable::default();
-}
-
-#[no_mangle]
-fn plugin_name() -> *const c_char {
- return PLUGIN_NAME.as_ptr();
-}
-
-#[no_mangle]
-fn plugin_id() -> *const c_char {
- return PLUGIN_ID.as_ptr();
-}
-
-#[no_mangle]
-fn plugin_init() -> c_int {
- return 0 as _;
-}
-
-#[no_mangle]
-fn plugin_dispose() -> c_int {
- return 0 as _;
-}
diff --git a/examples/ipc.rs b/examples/ipc.rs
new file mode 100644
index 00000000000..bca2321b124
--- /dev/null
+++ b/examples/ipc.rs
@@ -0,0 +1,90 @@
+use docopt::Docopt;
+use hbb_common::{
+ env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV},
+ log, tokio,
+};
+use librustdesk::{ipc::Data, *};
+
+const USAGE: &'static str = "
+IPC test program.
+
+Usage:
+ ipc (-s | --server | -c | --client) [-p | --postfix=]
+ ipc (-h | --help)
+
+Options:
+ -h --help Show this screen.
+ -s --server Run as IPC server.
+ -c --client Run as IPC client.
+ -p --postfix= IPC path postfix [default: ].
+";
+
+#[derive(Debug, serde::Deserialize)]
+struct Args {
+ flag_server: bool,
+ flag_client: bool,
+ flag_postfix: String,
+}
+
+#[tokio::main]
+async fn main() {
+ init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
+
+ let args: Args = Docopt::new(USAGE)
+ .and_then(|d| d.deserialize())
+ .unwrap_or_else(|e| e.exit());
+
+ if args.flag_server {
+ if args.flag_postfix.is_empty() {
+ log::info!("Starting IPC server...");
+ } else {
+ log::info!(
+ "Starting IPC server with postfix: '{}'...",
+ args.flag_postfix
+ );
+ }
+ ipc_server(&args.flag_postfix).await;
+ } else if args.flag_client {
+ if args.flag_postfix.is_empty() {
+ log::info!("Starting IPC client...");
+ } else {
+ log::info!(
+ "Starting IPC client with postfix: '{}'...",
+ args.flag_postfix
+ );
+ }
+ ipc_client(&args.flag_postfix).await;
+ }
+}
+
+async fn ipc_server(postfix: &str) {
+ let postfix = postfix.to_string();
+ let postfix2 = postfix.clone();
+ std::thread::spawn(move || {
+ if let Err(err) = crate::ipc::start(&postfix) {
+ log::error!("Failed to start ipc: {}", err);
+ std::process::exit(-1);
+ }
+ });
+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+ ipc_client(&postfix2).await;
+}
+
+async fn ipc_client(postfix: &str) {
+ loop {
+ match crate::ipc::connect(1000, postfix).await {
+ Ok(mut conn) => match conn.send(&Data::Empty).await {
+ Ok(_) => {
+ log::info!("send message to ipc server success");
+ }
+ Err(e) => {
+ log::error!("Failed to send message to ipc server: {}", e);
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to connect to ipc server: {}", e);
+ }
+ }
+ tokio::time::sleep(std::time::Duration::from_secs(6)).await;
+ }
+}
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
index 357fb37ab68..91e796adaf6 100644
--- a/fastlane/metadata/android/en-US/short_description.txt
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -1 +1 @@
-An open-source remote desktop application, the open source TeamViewer alternative.
+An open-source remote desktop application, the TeamViewer alternative
diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml
new file mode 100644
index 00000000000..0d3b33bb8c3
--- /dev/null
+++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml
@@ -0,0 +1,59 @@
+
+
+ com.rustdesk.RustDesk
+
+ RustDesk
+
+ com.rustdesk.RustDesk.desktop
+ CC0-1.0
+ AGPL-3.0-only
+ RustDesk
+ Secure remote desktop access
+
+
+ RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration.
+
+
+ - Works on Windows, macOS, Linux, iOS, Android, Web.
+ - Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
+ - Own your data, easily set up self-hosting solution on your infrastructure.
+ - P2P connection with end-to-end encryption based on NaCl.
+ - No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
+ - We like to keep things simple and will strive to make simpler where possible.
+
+
+ For self-hosting setup instructions please go to our home page.
+
+
+
+ Utility
+
+
+
+ Remote desktop session
+ https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png
+
+
+
+ #d9eaf8
+ #0160ee
+
+ https://rustdesk.com
+ https://github.com/rustdesk/rustdesk/issues
+ https://github.com/rustdesk/rustdesk/wiki/FAQ
+ https://rustdesk.com/docs
+ https://ko-fi.com/rustdesk
+ https://github.com/rustdesk/rustdesk
+ https://github.com/rustdesk/rustdesk/tree/master/src/lang
+ https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md
+ https://rustdesk.com/docs/en/technical-support
+
+ 600
+ always
+
+
+ keyboard
+ pointing
+
+
+
\ No newline at end of file
diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json
index 6d7acb5b89c..af1bc5fe74a 100644
--- a/flatpak/rustdesk.json
+++ b/flatpak/rustdesk.json
@@ -1,19 +1,30 @@
{
"id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform",
- "runtime-version": "23.08",
+ "runtime-version": "24.08",
"sdk": "org.freedesktop.Sdk",
"command": "rustdesk",
- "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
+ "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
+ "rename-desktop-file": "rustdesk.desktop",
+ "rename-icon": "rustdesk",
"modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
- "xdotool.json",
{
- "name": "pam",
- "buildsystem": "simple",
- "build-commands": [
- "./configure --disable-selinux --prefix=/app && make -j4 install"
- ],
+ "name": "xdotool",
+ "no-autogen": true,
+ "make-install-args": ["PREFIX=${FLATPAK_DEST}"],
+ "sources": [
+ {
+ "type": "archive",
+ "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
+ "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
+ }
+ ]
+ },
+ {
+ "name": "pam",
+ "buildsystem": "autotools",
+ "config-opts": ["--disable-selinux"],
"sources": [
{
"type": "archive",
@@ -26,32 +37,24 @@
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
- "bsdtar -zxvf rustdesk.deb",
- "tar -xvf ./data.tar.xz",
- "cp -r ./usr/* /app/",
- "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
- "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
- "mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
- "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
- "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
- "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
+ "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -",
+ "cp -r usr/* /app/",
+ "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk"
],
- "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [
{
"type": "file",
- "path": "./rustdesk.deb"
+ "path": "rustdesk.deb"
},
{
"type": "file",
- "path": "../res/scalable.svg"
+ "path": "com.rustdesk.RustDesk.metainfo.xml"
}
]
}
],
"finish-args": [
"--share=ipc",
- "--socket=x11",
"--socket=fallback-x11",
"--socket=wayland",
"--share=network",
@@ -60,4 +63,4 @@
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Flatpak"
]
-}
+}
\ No newline at end of file
diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json
deleted file mode 100644
index d7f41bf0ec0..00000000000
--- a/flatpak/xdotool.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "xdotool",
- "buildsystem": "simple",
- "build-commands": [
- "make -j4 && PREFIX=./build make install",
- "cp -r ./build/* /app/"
- ],
- "sources": [
- {
- "type": "archive",
- "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
- "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
- }
- ]
-}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
index f53f95d6754..3ca83fbac73 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
@@ -19,6 +19,7 @@ import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo
import android.view.KeyEvent as KeyEventAndroid
+import android.view.ViewConfiguration
import android.graphics.Rect
import android.media.AudioManager
import android.accessibilityservice.AccessibilityServiceInfo
@@ -34,10 +35,15 @@ import hbb.MessageOuterClass.KeyEvent
import hbb.MessageOuterClass.KeyboardMode
import hbb.KeyEventConverter
-const val LIFT_DOWN = 9
-const val LIFT_MOVE = 8
-const val LIFT_UP = 10
+// const val BUTTON_UP = 2
+// const val BUTTON_BACK = 0x08
+
+const val LEFT_DOWN = 9
+const val LEFT_MOVE = 8
+const val LEFT_UP = 10
const val RIGHT_UP = 18
+// (BUTTON_BACK << 3) | BUTTON_UP
+const val BACK_UP = 66
const val WHEEL_BUTTON_DOWN = 33
const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331
@@ -65,11 +71,14 @@ class InputService : AccessibilityService() {
private val logTag = "input service"
private var leftIsDown = false
private var touchPath = Path()
+ private var stroke: GestureDescription.StrokeDescription? = null
private var lastTouchGestureStartTime = 0L
private var mouseX = 0
private var mouseY = 0
private var timer = Timer()
private var recentActionTask: TimerTask? = null
+ // 100(tap timeout) + 400(long press timeout)
+ private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong()
private val wheelActionsQueue = LinkedList()
private var isWheelActionsPolling = false
@@ -77,6 +86,9 @@ class InputService : AccessibilityService() {
private var fakeEditTextForTextStateCalculation: EditText? = null
+ private var lastX = 0
+ private var lastY = 0
+
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
@RequiresApi(Build.VERSION_CODES.N)
@@ -84,7 +96,7 @@ class InputService : AccessibilityService() {
val x = max(0, _x)
val y = max(0, _y)
- if (mask == 0 || mask == LIFT_MOVE) {
+ if (mask == 0 || mask == LEFT_MOVE) {
val oldX = mouseX
val oldY = mouseY
mouseX = x * SCREEN_INFO.scale
@@ -98,31 +110,30 @@ class InputService : AccessibilityService() {
}
}
- // left button down ,was up
- if (mask == LIFT_DOWN) {
+ // left button down, was up
+ if (mask == LEFT_DOWN) {
isWaitingLongPress = true
timer.schedule(object : TimerTask() {
override fun run() {
if (isWaitingLongPress) {
isWaitingLongPress = false
- leftIsDown = false
- endGesture(mouseX, mouseY)
+ continueGesture(mouseX, mouseY)
}
}
- }, LONG_TAP_DELAY * 4)
+ }, longPressDuration)
leftIsDown = true
startGesture(mouseX, mouseY)
return
}
- // left down ,was down
+ // left down, was down
if (leftIsDown) {
continueGesture(mouseX, mouseY)
}
- // left up ,was down
- if (mask == LIFT_UP) {
+ // left up, was down
+ if (mask == LEFT_UP) {
if (leftIsDown) {
leftIsDown = false
isWaitingLongPress = false
@@ -132,6 +143,11 @@ class InputService : AccessibilityService() {
}
if (mask == RIGHT_UP) {
+ longPress(mouseX, mouseY)
+ return
+ }
+
+ if (mask == BACK_UP) {
performGlobalAction(GLOBAL_ACTION_BACK)
return
}
@@ -241,18 +257,100 @@ class InputService : AccessibilityService() {
}
}
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun performClick(x: Int, y: Int, duration: Long) {
+ val path = Path()
+ path.moveTo(x.toFloat(), y.toFloat())
+ try {
+ val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration)
+ val builder = GestureDescription.Builder()
+ builder.addStroke(longPressStroke)
+ Log.d(logTag, "performClick x:$x y:$y time:$duration")
+ dispatchGesture(builder.build(), null, null)
+ } catch (e: Exception) {
+ Log.e(logTag, "performClick, error:$e")
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun longPress(x: Int, y: Int) {
+ performClick(x, y, longPressDuration)
+ }
+
private fun startGesture(x: Int, y: Int) {
- touchPath = Path()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ touchPath.reset()
+ } else {
+ touchPath = Path()
+ }
touchPath.moveTo(x.toFloat(), y.toFloat())
lastTouchGestureStartTime = System.currentTimeMillis()
+ lastX = x
+ lastY = y
}
- private fun continueGesture(x: Int, y: Int) {
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) {
touchPath.lineTo(x.toFloat(), y.toFloat())
+ var duration = System.currentTimeMillis() - lastTouchGestureStartTime
+ if (duration <= 0) {
+ duration = 1
+ }
+ try {
+ if (stroke == null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stroke = GestureDescription.StrokeDescription(
+ touchPath,
+ 0,
+ duration,
+ willContinue
+ )
+ } else {
+ stroke = GestureDescription.StrokeDescription(
+ touchPath,
+ 0,
+ duration
+ )
+ }
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
+ } else {
+ stroke = null
+ stroke = GestureDescription.StrokeDescription(
+ touchPath,
+ 0,
+ duration
+ )
+ }
+ }
+ stroke?.let {
+ val builder = GestureDescription.Builder()
+ builder.addStroke(it)
+ Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration")
+ dispatchGesture(builder.build(), null, null)
+ }
+ } catch (e: Exception) {
+ Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e")
+ }
}
@RequiresApi(Build.VERSION_CODES.N)
- private fun endGesture(x: Int, y: Int) {
+ private fun continueGesture(x: Int, y: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ doDispatchGesture(x, y, true)
+ touchPath.reset()
+ touchPath.moveTo(x.toFloat(), y.toFloat())
+ lastTouchGestureStartTime = System.currentTimeMillis()
+ lastX = x
+ lastY = y
+ } else {
+ touchPath.lineTo(x.toFloat(), y.toFloat())
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun endGestureBelowO(x: Int, y: Int) {
try {
touchPath.lineTo(x.toFloat(), y.toFloat())
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
@@ -273,6 +371,17 @@ class InputService : AccessibilityService() {
}
}
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun endGesture(x: Int, y: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ doDispatchGesture(x, y, false)
+ touchPath.reset()
+ stroke = null
+ } else {
+ endGestureBelowO(x, y)
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.N)
fun onKeyEvent(data: ByteArray) {
val keyEvent = KeyEvent.parseFrom(data)
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
index 5c54c18fb82..a19c2ae9d06 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
@@ -316,7 +316,7 @@ class MainActivity : FlutterActivity() {
codecObject.put("mime_type", mime_type)
val caps = codec.getCapabilitiesForType(mime_type)
if (codec.isEncoder) {
- // Encoder‘s max_height and max_width are interchangeable
+ // Encoder's max_height and max_width are interchangeable
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
return@forEach
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
index e9ec0975d1b..7bb16a00ad6 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -65,8 +65,8 @@ class MainService : Service() {
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
- // turn on screen with LIFT_DOWN when screen off
- if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
+ // turn on screen with LEFT_DOWN when screen off
+ if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) {
if (wakeLock.isHeld) {
Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release()
@@ -122,9 +122,9 @@ class MainService : Service() {
val authorized = jsonObject["authorized"] as Boolean
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
val type = if (isFileTransfer) {
- translate("File Connection")
+ translate("Transfer file")
} else {
- translate("Screen Connection")
+ translate("Share screen")
}
if (authorized) {
if (!isFileTransfer && !isStart) {
diff --git a/flutter/android/gradle.properties b/flutter/android/gradle.properties
index 94adc3a3f97..804b29b300a 100644
--- a/flutter/android/gradle.properties
+++ b/flutter/android/gradle.properties
@@ -1,3 +1,4 @@
-org.gradle.jvmargs=-Xmx1536M
+org.gradle.jvmargs=-Xmx1024M
android.useAndroidX=true
android.enableJetifier=true
+org.gradle.daemon=false
diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle
index c5fb685a161..ae32fa00e5d 100644
--- a/flutter/android/settings.gradle
+++ b/flutter/android/settings.gradle
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "7.3.0" apply false
- id "org.jetbrains.kotlin.android" version "1.9.10" apply false
+ id "com.android.application" version "7.3.1" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.21" apply false
}
include ":app"
diff --git a/flutter/assets/device_group.ttf b/flutter/assets/device_group.ttf
new file mode 100644
index 00000000000..a6e42704f06
Binary files /dev/null and b/flutter/assets/device_group.ttf differ
diff --git a/flutter/assets/more.ttf b/flutter/assets/more.ttf
new file mode 100644
index 00000000000..3b01435df3a
Binary files /dev/null and b/flutter/assets/more.ttf differ
diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh
index 1821c529afb..ecfb444efea 100755
--- a/flutter/build_fdroid.sh
+++ b/flutter/build_fdroid.sh
@@ -150,6 +150,10 @@ prebuild)
# Flutter used to compile Flutter<->Rust bridge files
+ CARGO_EXPAND_VERSION="$(yq -r \
+ .env.CARGO_EXPAND_VERSION \
+ .github/workflows/bridge.yml)"
+
FLUTTER_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/bridge.yml)"
@@ -237,7 +241,10 @@ prebuild)
# Install rust bridge generator
- cargo install cargo-expand
+ cargo install \
+ cargo-expand \
+ --version "${CARGO_EXPAND_VERSION}" \
+ --locked
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid" \
diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh
index 50f2f0056b4..cd12626263d 100755
--- a/flutter/build_ios.sh
+++ b/flutter/build_ios.sh
@@ -4,4 +4,5 @@
# no obfuscate, because no easy to check errors
cd $(dirname $(dirname $(which flutter)))
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+cd -
flutter build ipa --release
diff --git a/flutter/deploy.sh b/flutter/deploy.sh
deleted file mode 100755
index f6826fd8720..00000000000
--- a/flutter/deploy.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-cd build/web/
-python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
-mv jds/dist/index.js ./
-mv jds/dist/vendor.js ./
-/bin/rm -rf js
-python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
-python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)'
-python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)'
-tar czf x *
-scp x sg:/tmp/
-ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
-/bin/rm x
-cd -
diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh
index 2d8410c7a4e..579baaa6dda 100755
--- a/flutter/ios_arm64.sh
+++ b/flutter/ios_arm64.sh
@@ -1,4 +1,2 @@
#!/usr/bin/env bash
-cd $(dirname $(dirname $(which flutter)))
-git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 208897ed039..c1dd7cd4ec6 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -29,8 +29,11 @@ import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
+import 'mobile/pages/view_camera_page.dart';
+import 'mobile/pages/terminal_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
+import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
@@ -96,6 +99,8 @@ enum DesktopType {
main,
remote,
fileTransfer,
+ viewCamera,
+ terminal,
cm,
portForward,
}
@@ -103,6 +108,10 @@ enum DesktopType {
class IconFont {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
+ static const _family3 = 'AddressBook';
+ static const _family4 = 'DeviceGroup';
+ static const _family5 = 'More';
+
IconFont._();
static const IconData max = IconData(0xe606, fontFamily: _family1);
@@ -113,8 +122,12 @@ class IconFont {
static const IconData menu = IconData(0xe628, fontFamily: _family1);
static const IconData search = IconData(0xe6a4, fontFamily: _family2);
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
- static const IconData addressBook =
- IconData(0xe602, fontFamily: "AddressBook");
+ static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
+ static const IconData deviceGroupOutline =
+ IconData(0xe623, fontFamily: _family4);
+ static const IconData deviceGroupFill =
+ IconData(0xe748, fontFamily: _family4);
+ static const IconData more = IconData(0xe609, fontFamily: _family5);
}
class ColorThemeExtension extends ThemeExtension {
@@ -817,7 +830,11 @@ class OverlayDialogManager {
close([res]) {
_dialogs.remove(dialogTag);
- dialog.complete(res);
+ try {
+ dialog.complete(res);
+ } catch (e) {
+ debugPrint("Dialog complete catch error: $e");
+ }
BackButtonInterceptor.removeByName(dialogTag);
}
@@ -1137,15 +1154,23 @@ Widget createDialogContent(String text) {
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
- {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
+ {bool? hasCancel,
+ ReconnectHandle? reconnect,
+ int? reconnectTimeout,
+ VoidCallback? onSubmit,
+ int? submitTimeout}) {
dialogManager.dismissAll();
List buttons = [];
bool hasOk = false;
submit() {
dialogManager.dismissAll();
- // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
- if (!type.contains("custom") && desktopType != DesktopType.portForward) {
- closeConnection();
+ if (onSubmit != null) {
+ onSubmit.call();
+ } else {
+ // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
+ if (!type.contains("custom") && desktopType != DesktopType.portForward) {
+ closeConnection();
+ }
}
}
@@ -1161,7 +1186,18 @@ void msgBox(SessionID sessionId, String type, String title, String text,
if (type != "connecting" && type != "success" && !type.contains("nook")) {
hasOk = true;
- buttons.insert(0, dialogButton('OK', onPressed: submit));
+ late final Widget btn;
+ if (submitTimeout != null) {
+ btn = _CountDownButton(
+ text: 'OK',
+ second: submitTimeout,
+ onPressed: submit,
+ submitOnTimeout: true,
+ );
+ } else {
+ btn = dialogButton('OK', onPressed: submit);
+ }
+ buttons.insert(0, btn);
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
@@ -1182,7 +1218,8 @@ void msgBox(SessionID sessionId, String type, String title, String text,
reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
- final button = Obx(() => _ReconnectCountDownButton(
+ final button = Obx(() => _CountDownButton(
+ text: 'Reconnect',
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
@@ -1536,7 +1573,9 @@ bool option2bool(String option, String value) {
String bool2option(String option, bool b) {
String res;
- if (option.startsWith('enable-')) {
+ if (option.startsWith('enable-') &&
+ option != kOptionEnableUdpPunch &&
+ option != kOptionEnableIpv6Punch) {
res = b ? defaultOptionYes : 'N';
} else if (option.startsWith('allow-') ||
option == kOptionStopService ||
@@ -1544,7 +1583,9 @@ String bool2option(String option, bool b) {
option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : defaultOptionNo;
} else {
- assert(false);
+ if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
+ assert(false);
+ }
res = b ? 'Y' : 'N';
}
return res;
@@ -1741,7 +1782,8 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async {
await bind.setLocalFlutterOption(
k: windowFramePrefix + type.name, v: pos.toString());
- if (type == WindowType.RemoteDesktop && windowId != null) {
+ if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
+ windowId != null) {
await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos);
}
@@ -1892,7 +1934,9 @@ Future restoreWindowPosition(WindowType type,
String? pos;
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
// Though "open in tabs" is true and the new window restore peer position, it's ok.
- if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
+ if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
+ windowId != null &&
+ peerId != null) {
final peerPos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: windowFramePrefix + type.name);
if (peerPos.isNotEmpty) {
@@ -1907,7 +1951,7 @@ Future restoreWindowPosition(WindowType type,
debugPrint("no window position saved, ignoring position restoration");
return false;
}
- if (type == WindowType.RemoteDesktop) {
+ if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
if (!isRemotePeerPos && windowId != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
@@ -2076,8 +2120,14 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
enum UriLinkType {
remoteDesktop,
fileTransfer,
+ viewCamera,
portForward,
rdp,
+ terminal,
+}
+
+setEnvTerminalAdmin() {
+ bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y');
}
// uri link handler
@@ -2127,6 +2177,11 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1];
i++;
break;
+ case '--view-camera':
+ type = UriLinkType.viewCamera;
+ id = args[i + 1];
+ i++;
+ break;
case '--port-forward':
type = UriLinkType.portForward;
id = args[i + 1];
@@ -2137,6 +2192,17 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1];
i++;
break;
+ case '--terminal':
+ type = UriLinkType.terminal;
+ id = args[i + 1];
+ i++;
+ break;
+ case '--terminal-admin':
+ setEnvTerminalAdmin();
+ type = UriLinkType.terminal;
+ id = args[i + 1];
+ i++;
+ break;
case '--password':
password = args[i + 1];
i++;
@@ -2168,6 +2234,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
password: password, forceRelay: forceRelay);
});
break;
+ case UriLinkType.viewCamera:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newViewCamera(id!,
+ password: password, forceRelay: forceRelay);
+ });
+ break;
case UriLinkType.portForward:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newPortForward(id!, false,
@@ -2180,6 +2252,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
password: password, forceRelay: forceRelay);
});
break;
+ case UriLinkType.terminal:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newTerminal(id!,
+ password: password, forceRelay: forceRelay);
+ });
+ break;
}
return true;
@@ -2191,7 +2269,16 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
List? urlLinkToCmdArgs(Uri uri) {
String? command;
String? id;
- final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
+ final options = [
+ "connect",
+ "play",
+ "file-transfer",
+ "view-camera",
+ "port-forward",
+ "rdp",
+ "terminal",
+ "terminal-admin",
+ ];
if (uri.authority.isEmpty &&
uri.path.split('').every((char) => char == '/')) {
return [];
@@ -2219,19 +2306,10 @@ List? urlLinkToCmdArgs(Uri uri) {
}
}
} else if (options.contains(uri.authority)) {
- final optionIndex = options.indexOf(uri.authority);
command = '--${uri.authority}';
if (uri.path.length > 1) {
id = uri.path.substring(1);
}
- if (isMobile && id != null) {
- if (optionIndex == 0 || optionIndex == 1) {
- connect(Get.context!, id);
- } else if (optionIndex == 2) {
- connect(Get.context!, id, isFileTransfer: true);
- }
- return null;
- }
} else if (uri.authority.length > 2 &&
(uri.path.length <= 1 ||
(uri.path == '/r' || uri.path.startsWith('/r@')))) {
@@ -2255,12 +2333,29 @@ List? urlLinkToCmdArgs(Uri uri) {
}
}
- if (isMobile) {
- if (id != null) {
- final forceRelay = queryParameters["relay"] != null;
- connect(Get.context!, id, forceRelay: forceRelay);
- return null;
+ if (isMobile && id != null) {
+ final forceRelay = queryParameters["relay"] != null;
+ final password = queryParameters["password"];
+
+ // Determine connection type based on command
+ if (command == '--file-transfer') {
+ connect(Get.context!, id,
+ isFileTransfer: true, forceRelay: forceRelay, password: password);
+ } else if (command == '--view-camera') {
+ connect(Get.context!, id,
+ isViewCamera: true, forceRelay: forceRelay, password: password);
+ } else if (command == '--terminal') {
+ connect(Get.context!, id,
+ isTerminal: true, forceRelay: forceRelay, password: password);
+ } else if (command == 'terminal-admin') {
+ setEnvTerminalAdmin();
+ connect(Get.context!, id,
+ isTerminal: true, forceRelay: forceRelay, password: password);
+ } else {
+ // Default to remote desktop for '--connect', '--play', or direct connection
+ connect(Get.context!, id, forceRelay: forceRelay, password: password);
}
+ return null;
}
List args = List.empty(growable: true);
@@ -2281,6 +2376,8 @@ List? urlLinkToCmdArgs(Uri uri) {
connectMainDesktop(String id,
{required bool isFileTransfer,
+ required bool isViewCamera,
+ required bool isTerminal,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay,
@@ -2293,12 +2390,24 @@ connectMainDesktop(String id,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
+ } else if (isViewCamera) {
+ await rustDeskWinManager.newViewCamera(id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ connToken: connToken,
+ forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP,
password: password,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
+ } else if (isTerminal) {
+ await rustDeskWinManager.newTerminal(id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ connToken: connToken,
+ forceRelay: forceRelay);
} else {
await rustDeskWinManager.newRemoteDesktop(id,
password: password,
@@ -2309,10 +2418,13 @@ connectMainDesktop(String id,
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
+/// If [isViewCamera], starts a session only for view camera.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
connect(BuildContext context, String id,
{bool isFileTransfer = false,
+ bool isViewCamera = false,
+ bool isTerminal = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceRelay = false,
@@ -2335,7 +2447,7 @@ connect(BuildContext context, String id,
id = id.replaceAll(' ', '');
final oldId = id;
id = await bind.mainHandleRelayId(id: id);
- final forceRelay2 = id != oldId || forceRelay;
+ forceRelay = id != oldId || forceRelay;
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
@@ -2344,16 +2456,20 @@ connect(BuildContext context, String id,
await connectMainDesktop(
id,
isFileTransfer: isFileTransfer,
+ isViewCamera: isViewCamera,
+ isTerminal: isTerminal,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
password: password,
isSharedPassword: isSharedPassword,
- forceRelay: forceRelay2,
+ forceRelay: forceRelay,
);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
+ 'isViewCamera': isViewCamera,
+ 'isTerminal': isTerminal,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
'password': password,
@@ -2387,10 +2503,52 @@ connect(BuildContext context, String id,
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(
- id: id, password: password, isSharedPassword: isSharedPassword),
+ id: id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ forceRelay: forceRelay),
),
);
}
+ } else if (isViewCamera) {
+ if (isWeb) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) =>
+ desktop_view_camera.ViewCameraPage(
+ key: ValueKey(id),
+ id: id,
+ toolbarState: ToolbarState(),
+ password: password,
+ isSharedPassword: isSharedPassword,
+ ),
+ ),
+ );
+ } else {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => ViewCameraPage(
+ id: id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ forceRelay: forceRelay),
+ ),
+ );
+ }
+ } else if (isTerminal) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => TerminalPage(
+ id: id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ forceRelay: forceRelay,
+ ),
+ ),
+ );
} else {
if (isWeb) {
Navigator.push(
@@ -2401,7 +2559,6 @@ connect(BuildContext context, String id,
id: id,
toolbarState: ToolbarState(),
password: password,
- forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
),
),
@@ -2411,7 +2568,10 @@ connect(BuildContext context, String id,
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(
- id: id, password: password, isSharedPassword: isSharedPassword),
+ id: id,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ forceRelay: forceRelay),
),
);
}
@@ -2566,6 +2726,8 @@ bool get kUseCompatibleUiMode =>
isWindows &&
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
+bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10;
+
class ServerConfig {
late String idServer;
late String relayServer;
@@ -2675,6 +2837,8 @@ String getWindowName({WindowType? overrideType}) {
return name;
case WindowType.FileTransfer:
return "File Transfer - $name";
+ case WindowType.ViewCamera:
+ return "View Camera - $name";
case WindowType.PortForward:
return "Port Forward - $name";
case WindowType.RemoteDesktop:
@@ -2779,6 +2943,7 @@ Future canBeBlocked() async {
return access_mode == 'view' || (access_mode.isEmpty && !option);
}
+// to-do: web not implemented
Future shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
if (use != null && !await use()) {
block.value = false;
@@ -3040,6 +3205,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
'peer_id': peerId,
'display': i,
'display_count': pi.displays.length,
+ 'window_type': (kWindowType ?? WindowType.RemoteDesktop).index,
};
if (screenRect != null) {
args['screen_rect'] = {
@@ -3054,12 +3220,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
}
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
- int? display, Rect? screenRect) async {
+ WindowType windowType, int? display, Rect? screenRect) async {
if (screenRect == null) {
// Do not restore window position to new connection if there's a pre-session.
// https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) {
- await restoreWindowPosition(WindowType.RemoteDesktop,
+ await restoreWindowPosition(windowType,
windowId: windowId, display: display, peerId: peerId);
}
} else {
@@ -3103,21 +3269,24 @@ parseParamScreenRect(Map params) {
get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
-class _ReconnectCountDownButton extends StatefulWidget {
- _ReconnectCountDownButton({
+class _CountDownButton extends StatefulWidget {
+ _CountDownButton({
Key? key,
+ required this.text,
required this.second,
required this.onPressed,
+ this.submitOnTimeout = false,
}) : super(key: key);
+ final String text;
final VoidCallback? onPressed;
final int second;
+ final bool submitOnTimeout;
@override
- State<_ReconnectCountDownButton> createState() =>
- _ReconnectCountDownButtonState();
+ State<_CountDownButton> createState() => _CountDownButtonState();
}
-class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
+class _CountDownButtonState extends State<_CountDownButton> {
late int _countdownSeconds = widget.second;
Timer? _timer;
@@ -3138,6 +3307,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdownSeconds <= 0) {
timer.cancel();
+ if (widget.submitOnTimeout) {
+ widget.onPressed?.call();
+ }
} else {
setState(() {
_countdownSeconds--;
@@ -3149,7 +3321,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
@override
Widget build(BuildContext context) {
return dialogButton(
- '${translate('Reconnect')} (${_countdownSeconds}s)',
+ '${translate(widget.text)} (${_countdownSeconds}s)',
onPressed: widget.onPressed,
isOutline: true,
);
@@ -3339,6 +3511,9 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
}
Widget loadPowered(BuildContext context) {
+ if (bind.mainGetBuildinOption(key: "hide-powered-by-me") == 'Y') {
+ return SizedBox.shrink();
+ }
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
@@ -3610,7 +3785,7 @@ void earlyAssert() {
}
void checkUpdate() {
- if (isDesktop || isAndroid) {
+ if (!isWeb) {
if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
@@ -3625,3 +3800,134 @@ void checkUpdate() {
}
}
}
+
+// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535
+// For TextField, TextFormField
+extension WorkaroundFreezeLinuxMint on Widget {
+ Widget workaroundFreezeLinuxMint() {
+ // No need to check if is Linux Mint, because this workaround is harmless on other platforms.
+ if (isLinux) {
+ return ExcludeSemantics(child: this);
+ } else {
+ return this;
+ }
+ }
+}
+
+// Don't use `extension` here, the border looks weird if using `extension` in my test.
+Widget workaroundWindowBorder(BuildContext context, Widget child) {
+ if (!isWin10) {
+ return child;
+ }
+
+ final isLight = Theme.of(context).brightness == Brightness.light;
+ final borderColor = isLight ? Colors.black87 : Colors.grey;
+ final width = isLight ? 0.5 : 0.1;
+
+ getBorderWidget(Widget child) {
+ return Obx(() =>
+ (stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue)
+ ? Offstage()
+ : child);
+ }
+
+ final List borders = [
+ getBorderWidget(Container(
+ color: borderColor,
+ height: width + 0.1,
+ ))
+ ];
+ if (kWindowType == WindowType.Main && !isLight) {
+ borders.addAll([
+ getBorderWidget(Align(
+ alignment: Alignment.topLeft,
+ child: Container(
+ color: borderColor,
+ width: width,
+ ),
+ )),
+ getBorderWidget(Align(
+ alignment: Alignment.topRight,
+ child: Container(
+ color: borderColor,
+ width: width,
+ ),
+ )),
+ getBorderWidget(Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ color: borderColor,
+ height: width,
+ ),
+ )),
+ ]);
+ }
+ return Stack(
+ children: [
+ child,
+ ...borders,
+ ],
+ );
+}
+
+void updateTextAndPreserveSelection(
+ TextEditingController controller, String text) {
+ // Only care about select all for now.
+ final isSelected = controller.selection.isValid &&
+ controller.selection.end > controller.selection.start;
+
+ // Set text will make the selection invalid.
+ controller.text = text;
+
+ if (isSelected) {
+ controller.selection = TextSelection(
+ baseOffset: 0, extentOffset: controller.value.text.length);
+ }
+}
+
+List getPrinterNames() {
+ final printerNamesJson = bind.mainGetPrinterNames();
+ if (printerNamesJson.isEmpty) {
+ return [];
+ }
+ try {
+ final List printerNamesList = jsonDecode(printerNamesJson);
+ final appPrinterName = '$appName Printer';
+ return printerNamesList
+ .map((e) => e.toString())
+ .where((name) => name != appPrinterName)
+ .toList();
+ } catch (e) {
+ debugPrint('failed to parse printer names, err: $e');
+ return [];
+ }
+}
+
+String _appName = '';
+String get appName {
+ if (_appName.isEmpty) {
+ _appName = bind.mainGetAppNameSync();
+ }
+ return _appName;
+}
+
+String getConnectionText(bool secure, bool direct, String streamType) {
+ String connectionText;
+ if (secure && direct) {
+ connectionText = translate("Direct and encrypted connection");
+ } else if (secure && !direct) {
+ connectionText = translate("Relayed and encrypted connection");
+ } else if (!secure && direct) {
+ connectionText = translate("Direct and unencrypted connection");
+ } else {
+ connectionText = translate("Relayed and unencrypted connection");
+ }
+ if (streamType == 'Relay') {
+ streamType = 'TCP';
+ }
+ if (streamType.isEmpty) {
+ return connectionText;
+ } else {
+ return '$connectionText ($streamType)';
+ }
+}
diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart
index e189cc7b23f..97baf546a64 100644
--- a/flutter/lib/common/hbbs/hbbs.dart
+++ b/flutter/lib/common/hbbs/hbbs.dart
@@ -27,6 +27,7 @@ class UserPayload {
String name = '';
String email = '';
String note = '';
+ String? verifier;
UserStatus status;
bool isAdmin = false;
@@ -34,6 +35,7 @@ class UserPayload {
: name = json['name'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
+ verifier = json['verifier'],
status = json['status'] == 0
? UserStatus.kDisabled
: json['status'] == -1
@@ -67,6 +69,7 @@ class PeerPayload {
int? status;
String user = '';
String user_name = '';
+ String? device_group_name;
String note = '';
PeerPayload.fromJson(Map json)
@@ -75,6 +78,7 @@ class PeerPayload {
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
+ device_group_name = json['device_group_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
@@ -84,6 +88,7 @@ class PeerPayload {
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
+ "device_group_name": p.device_group_name,
});
}
@@ -265,3 +270,19 @@ class AbTag {
: name = json['name'] ?? '',
color = json['color'] ?? '';
}
+
+class DeviceGroupPayload {
+ String name;
+
+ DeviceGroupPayload(this.name);
+
+ DeviceGroupPayload.fromJson(Map json)
+ : name = json['name'] ?? '';
+
+ Map toGroupCacheJson() {
+ final Map map = {
+ 'name': name,
+ };
+ return map;
+ }
+}
diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart
index 908c98a70e3..4f9373ccd4e 100644
--- a/flutter/lib/common/shared_state.dart
+++ b/flutter/lib/common/shared_state.dart
@@ -77,9 +77,11 @@ class CurrentDisplayState {
class ConnectionType {
final Rx _secure = kInvalidValueStr.obs;
final Rx _direct = kInvalidValueStr.obs;
+ final Rx _stream_type = kInvalidValueStr.obs;
Rx get secure => _secure;
Rx get direct => _direct;
+ Rx get stream_type => _stream_type;
static String get strSecure => 'secure';
static String get strInsecure => 'insecure';
@@ -94,9 +96,14 @@ class ConnectionType {
_direct.value = v ? strDirect : strIndirect;
}
+ void setStreamType(String v) {
+ _stream_type.value = v;
+ }
+
bool isValid() {
return _secure.value != kInvalidValueStr &&
- _direct.value != kInvalidValueStr;
+ _direct.value != kInvalidValueStr &&
+ _stream_type.value != kInvalidValueStr;
}
}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index ae07c1498cf..6a3cec8ade8 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -286,7 +286,7 @@ class _AddressBookState extends State {
borderRadius: BorderRadius.circular(8),
),
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
searchMatchFn: (item, searchValue) {
return item.value
@@ -509,13 +509,13 @@ class _AddressBookState extends State {
double marginBottom = 4;
- row({required Widget lable, required Widget input}) {
+ row({required Widget label, required Widget input}) {
makeChild(bool isPortrait) => Row(
children: [
!isPortrait
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
- child: lable.marginOnly(right: 10))
+ child: label.marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: ConstrainedBox(
@@ -535,7 +535,7 @@ class _AddressBookState extends State {
Column(
children: [
row(
- lable: Row(
+ label: Row(
children: [
Text(
'*',
@@ -556,9 +556,9 @@ class _AddressBookState extends State {
: translate('ID'),
errorText: errorMsg,
errorMaxLines: 5),
- ))),
+ ).workaroundFreezeLinuxMint())),
row(
- lable: Text(
+ label: Text(
translate('Alias'),
style: style,
),
@@ -569,11 +569,11 @@ class _AddressBookState extends State {
? null
: translate('Alias'),
),
- )),
+ ).workaroundFreezeLinuxMint()),
),
if (isCurrentAbShared)
row(
- lable: Text(
+ label: Text(
translate('Password'),
style: style,
),
@@ -598,7 +598,7 @@ class _AddressBookState extends State {
},
),
),
- ),
+ ).workaroundFreezeLinuxMint(),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
@@ -704,7 +704,7 @@ class _AddressBookState extends State {
),
controller: controller,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
),
diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart
index 978d053df4b..ec64cca1892 100644
--- a/flutter/lib/common/widgets/autocomplete.dart
+++ b/flutter/lib/common/widgets/autocomplete.dart
@@ -1,4 +1,3 @@
-import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart';
@@ -6,56 +5,104 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
-Future> getAllPeers() async {
- Map recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
- Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
- Map combinedPeers = {};
-
- void mergePeers(Map peers) {
- if (peers.containsKey("peers")) {
- dynamic peerData = peers["peers"];
-
- if (peerData is String) {
- try {
- peerData = jsonDecode(peerData);
- } catch (e) {
- print("Error decoding peers: $e");
- return;
- }
- }
+class AllPeersLoader {
+ List peers = [];
- if (peerData is List) {
- for (var peer in peerData) {
- if (peer is Map && peer.containsKey("id")) {
- String id = peer["id"];
- if (!combinedPeers.containsKey(id)) {
- combinedPeers[id] = peer;
- }
- }
- }
- }
- }
+ bool _isPeersLoading = false;
+ bool _isPeersLoaded = false;
+
+ final String _listenerKey = 'AllPeersLoader';
+
+ late void Function(VoidCallback) setState;
+
+ bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
+ bool get isPeersLoaded => _isPeersLoaded;
+
+ AllPeersLoader();
+
+ void init(void Function(VoidCallback) setState) {
+ this.setState = setState;
+ gFFI.recentPeersModel.addListener(_mergeAllPeers);
+ gFFI.lanPeersModel.addListener(_mergeAllPeers);
+ gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
+ gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
}
- mergePeers(recentPeers);
- mergePeers(lanPeers);
- for (var p in gFFI.abModel.allPeers()) {
- if (!combinedPeers.containsKey(p.id)) {
- combinedPeers[p.id] = p.toJson();
- }
+ void clear() {
+ gFFI.recentPeersModel.removeListener(_mergeAllPeers);
+ gFFI.lanPeersModel.removeListener(_mergeAllPeers);
+ gFFI.abModel.removePeerUpdateListener(_listenerKey);
+ gFFI.groupModel.removePeerUpdateListener(_listenerKey);
}
- for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
- if (!combinedPeers.containsKey(p.id)) {
- combinedPeers[p.id] = p.toJson();
+
+ Future getAllPeers() async {
+ if (!needLoad) {
+ return;
+ }
+ _isPeersLoading = true;
+
+ if (gFFI.recentPeersModel.peers.isEmpty) {
+ bind.mainLoadRecentPeers();
+ }
+ if (gFFI.lanPeersModel.peers.isEmpty) {
+ bind.mainLoadLanPeers();
+ }
+ // No need to care about peers from abModel, and group model.
+ // Because they will pull data in `refreshCurrentUser()` on startup.
+
+ final startTime = DateTime.now();
+ _mergeAllPeers();
+ final diffTime = DateTime.now().difference(startTime).inMilliseconds;
+ if (diffTime < 100) {
+ await Future.delayed(Duration(milliseconds: diffTime));
}
}
- List parsedPeers = [];
+ void _mergeAllPeers() {
+ Map combinedPeers = {};
+ for (var p in gFFI.abModel.allPeers()) {
+ if (!combinedPeers.containsKey(p.id)) {
+ combinedPeers[p.id] = p.toJson();
+ }
+ }
+ for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
+ if (!combinedPeers.containsKey(p.id)) {
+ combinedPeers[p.id] = p.toJson();
+ }
+ }
+
+ List parsedPeers = [];
+ for (var peer in combinedPeers.values) {
+ parsedPeers.add(Peer.fromJson(peer));
+ }
+
+ Set peerIds = combinedPeers.keys.toSet();
+ for (final peer in gFFI.lanPeersModel.peers) {
+ if (!peerIds.contains(peer.id)) {
+ parsedPeers.add(peer);
+ peerIds.add(peer.id);
+ }
+ }
+
+ for (final peer in gFFI.recentPeersModel.peers) {
+ if (!peerIds.contains(peer.id)) {
+ parsedPeers.add(peer);
+ peerIds.add(peer.id);
+ }
+ }
+ for (final id in gFFI.recentPeersModel.restPeerIds) {
+ if (!peerIds.contains(id)) {
+ parsedPeers.add(Peer.fromJson({'id': id}));
+ peerIds.add(id);
+ }
+ }
- for (var peer in combinedPeers.values) {
- parsedPeers.add(Peer.fromJson(peer));
+ peers = parsedPeers;
+ setState(() {
+ _isPeersLoading = false;
+ _isPeersLoaded = true;
+ });
}
- return parsedPeers;
}
class AutocompletePeerTile extends StatefulWidget {
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index b6611d3ede3..4b0954d40b1 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape {
);
},
),
- );
+ ).workaroundFreezeLinuxMint();
return SelectionArea(child: chat);
}),
],
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index cc3e0613105..fe0b799ac49 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
@@ -71,7 +70,7 @@ void changeIdDialog() {
final rules = [
RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
LengthRangeValidationRule(6, 16),
- RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
+ RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$'))
];
gFFI.dialogManager.show((setState, close, context) {
@@ -140,7 +139,7 @@ void changeIdDialog() {
msg = '';
});
},
- ),
+ ).workaroundFreezeLinuxMint(),
const SizedBox(
height: 8.0,
),
@@ -201,13 +200,14 @@ void changeWhiteList({Function()? callback}) async {
children: [
Expanded(
child: TextField(
- maxLines: null,
- decoration: InputDecoration(
- errorText: msg.isEmpty ? null : translate(msg),
- ),
- controller: controller,
- enabled: !isOptFixed,
- autofocus: true),
+ maxLines: null,
+ decoration: InputDecoration(
+ errorText: msg.isEmpty ? null : translate(msg),
+ ),
+ controller: controller,
+ enabled: !isOptFixed,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -287,22 +287,23 @@ Future changeDirectAccessPort(
children: [
Expanded(
child: TextField(
- maxLines: null,
- keyboardType: TextInputType.number,
- decoration: InputDecoration(
- hintText: '21118',
- isCollapsed: true,
- prefix: Text('$currentIP : '),
- suffix: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.clear, size: 16),
- onPressed: () => controller.clear())),
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(
- r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
- ],
- controller: controller,
- autofocus: true),
+ maxLines: null,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ hintText: '21118',
+ isCollapsed: true,
+ prefix: Text('$currentIP : '),
+ suffix: IconButton(
+ padding: EdgeInsets.zero,
+ icon: const Icon(Icons.clear, size: 16),
+ onPressed: () => controller.clear())),
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(
+ r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
+ ],
+ controller: controller,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -335,21 +336,22 @@ Future changeAutoDisconnectTimeout(String old) async {
children: [
Expanded(
child: TextField(
- maxLines: null,
- keyboardType: TextInputType.number,
- decoration: InputDecoration(
- hintText: '10',
- isCollapsed: true,
- suffix: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.clear, size: 16),
- onPressed: () => controller.clear())),
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(
- r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
- ],
- controller: controller,
- autofocus: true),
+ maxLines: null,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ hintText: '10',
+ isCollapsed: true,
+ suffix: IconButton(
+ padding: EdgeInsets.zero,
+ icon: const Icon(Icons.clear, size: 16),
+ onPressed: () => controller.clear())),
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(
+ r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
+ ],
+ controller: controller,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -409,25 +411,39 @@ class DialogTextField extends StatelessWidget {
return Row(
children: [
Expanded(
- child: TextField(
- decoration: InputDecoration(
- labelText: title,
- hintText: hintText,
- prefixIcon: prefixIcon,
- suffixIcon: suffixIcon,
- helperText: helperText,
- helperMaxLines: 8,
- errorText: errorText,
- errorMaxLines: 8,
- ),
- controller: controller,
- focusNode: focusNode,
- autofocus: true,
- obscureText: obscureText,
- keyboardType: keyboardType,
- inputFormatters: inputFormatters,
- maxLength: maxLength,
- ),
+ child: Column(
+ children: [
+ TextField(
+ decoration: InputDecoration(
+ labelText: title,
+ hintText: hintText,
+ prefixIcon: prefixIcon,
+ suffixIcon: suffixIcon,
+ helperText: helperText,
+ helperMaxLines: 8,
+ ),
+ controller: controller,
+ focusNode: focusNode,
+ autofocus: true,
+ obscureText: obscureText,
+ keyboardType: keyboardType,
+ inputFormatters: inputFormatters,
+ maxLength: maxLength,
+ ),
+ if (errorText != null)
+ Align(
+ alignment: Alignment.centerLeft,
+ child: SelectableText(
+ errorText!,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.error,
+ fontSize: 12,
+ ),
+ textAlign: TextAlign.left,
+ ).paddingOnly(top: 8, left: 12),
+ ),
+ ],
+ ).workaroundFreezeLinuxMint(),
),
],
).paddingSymmetric(vertical: 4.0);
@@ -803,23 +819,33 @@ void enterPasswordDialog(
}
void enterUserLoginDialog(
- SessionID sessionId, OverlayDialogManager dialogManager) async {
+ SessionID sessionId,
+ OverlayDialogManager dialogManager,
+ String osAccountDescTip,
+ bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
+ osAccountDescTip: osAccountDescTip,
+ canRememberAccount: canRememberAccount,
);
}
void enterUserLoginAndPasswordDialog(
- SessionID sessionId, OverlayDialogManager dialogManager) async {
+ SessionID sessionId,
+ OverlayDialogManager dialogManager,
+ String osAccountDescTip,
+ bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
passwordController: TextEditingController(),
+ osAccountDescTip: osAccountDescTip,
+ canRememberAccount: canRememberAccount,
);
}
@@ -829,17 +855,28 @@ _connectDialog(
TextEditingController? osUsernameController,
TextEditingController? osPasswordController,
TextEditingController? passwordController,
+ String? osAccountDescTip,
+ bool canRememberAccount = true,
}) async {
+ final errUsername = ''.obs;
var rememberPassword = false;
if (passwordController != null) {
rememberPassword =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
var rememberAccount = false;
- if (osUsernameController != null) {
+ if (canRememberAccount && osUsernameController != null) {
rememberAccount =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
+ if (osUsernameController != null) {
+ osUsernameController.addListener(() {
+ if (errUsername.value.isNotEmpty) {
+ errUsername.value = '';
+ }
+ });
+ }
+
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
cancel() {
@@ -848,6 +885,13 @@ _connectDialog(
}
submit() {
+ if (osUsernameController != null) {
+ if (osUsernameController.text.trim().isEmpty) {
+ errUsername.value = translate('Empty Username');
+ setState(() {});
+ return;
+ }
+ }
final osUsername = osUsernameController?.text.trim() ?? '';
final osPassword = osPasswordController?.text.trim() ?? '';
final password = passwordController?.text.trim() ?? '';
@@ -911,26 +955,39 @@ _connectDialog(
}
return Column(
children: [
- descWidget(translate('login_linux_tip')),
+ if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)),
DialogTextField(
title: translate(DialogTextField.kUsernameTitle),
controller: osUsernameController,
prefixIcon: DialogTextField.kUsernameIcon,
errorText: null,
),
+ if (errUsername.value.isNotEmpty)
+ Align(
+ alignment: Alignment.centerLeft,
+ child: SelectableText(
+ errUsername.value,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.error,
+ fontSize: 12,
+ ),
+ textAlign: TextAlign.left,
+ ).paddingOnly(left: 12, bottom: 2),
+ ),
PasswordWidget(
controller: osPasswordController,
autoFocus: false,
),
- rememberWidget(
- translate('remember_account_tip'),
- rememberAccount,
- (v) {
- if (v != null) {
- setState(() => rememberAccount = v);
- }
- },
- ),
+ if (canRememberAccount)
+ rememberWidget(
+ translate('remember_account_tip'),
+ rememberAccount,
+ (v) {
+ if (v != null) {
+ setState(() => rememberAccount = v);
+ }
+ },
+ ),
],
);
}
@@ -1120,7 +1177,7 @@ void showRequestElevationDialog(
DialogTextField(
controller: userController,
title: translate('Username'),
- hintText: translate('eg: admin'),
+ hintText: translate('elevation_username_tip'),
prefixIcon: DialogTextField.kUsernameIcon,
errorText: errUser.isEmpty ? null : errUser.value,
),
@@ -1501,7 +1558,7 @@ showAuditDialog(FFI ffi) async {
maxLength: 256,
controller: controller,
focusNode: focusNode,
- )),
+ ).workaroundFreezeLinuxMint()),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
@@ -1607,6 +1664,28 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}
+trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
+ int initSpeed = ffi.inputModel.trackpadSpeed;
+ final curSpeed = SimpleWrapper(initSpeed);
+ final btnClose = dialogButton('Close', onPressed: () async {
+ if (curSpeed.value <= kMaxTrackpadSpeed &&
+ curSpeed.value >= kMinTrackpadSpeed &&
+ curSpeed.value != initSpeed) {
+ await bind.sessionSetTrackpadSpeed(
+ sessionId: sessionId, value: curSpeed.value);
+ await ffi.inputModel.updateTrackpadSpeed();
+ }
+ ffi.dialogManager.dismissAll();
+ });
+ msgBoxCommon(
+ ffi.dialogManager,
+ 'Trackpad speed',
+ TrackpadSpeedWidget(
+ value: curSpeed,
+ ),
+ [btnClose]);
+}
+
void deleteConfirmDialog(Function onSubmit, String title) async {
gFFI.dialogManager.show(
(setState, close, context) {
@@ -1748,7 +1827,7 @@ void renameDialog(
autofocus: true,
decoration: InputDecoration(labelText: translate('Name')),
validator: validator,
- ),
+ ).workaroundFreezeLinuxMint(),
),
),
// NOT use Offstage to wrap LinearProgressIndicator
@@ -1808,7 +1887,7 @@ void changeBot({Function()? callback}) async {
decoration: InputDecoration(
hintText: translate('Token'),
),
- );
+ ).workaroundFreezeLinuxMint();
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
@@ -2178,7 +2257,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
},
),
),
- ),
+ ).workaroundFreezeLinuxMint(),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index 71f3dacc3b2..c1f18f0a82d 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -166,10 +166,13 @@ class _WidgetOPState extends State {
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
+ final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false;
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
- launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
+ if (!urlLaunched) {
+ launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
+ }
_url = url;
}
if (authBody != null) {
@@ -455,10 +458,14 @@ Future loginDialog() async {
resp.user, resp.secret, isEmailVerification);
} else {
setState(() => isInProgress = false);
+ // Workaround for web, close the dialog first, then show the verification code dialog.
+ // Otherwise, the text field will keep selecting the text and we can't input the code.
+ // Not sure why this happens.
+ if (isWeb && close != null) close(null);
final res = await verificationCodeDialog(
resp.user, resp.secret, isEmailVerification);
if (res == true) {
- if (close != null) close(false);
+ if (!isWeb && close != null) close(false);
return;
}
}
@@ -678,7 +685,7 @@ Future verificationCodeDialog(
labelText: "Email", prefixIcon: Icon(Icons.email)),
readOnly: true,
controller: TextEditingController(text: user?.email),
- )),
+ ).workaroundFreezeLinuxMint()),
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
codeField,
/*
diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart
index 867d71dff2d..6207a736321 100644
--- a/flutter/lib/common/widgets/my_group.dart
+++ b/flutter/lib/common/widgets/my_group.dart
@@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget {
}
class _MyGroupState extends State {
- RxString get selectedUser => gFFI.groupModel.selectedUser;
- RxString get searchUserText => gFFI.groupModel.searchUserText;
+ RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup;
+ RxString get selectedAccessibleItemName =>
+ gFFI.groupModel.selectedAccessibleItemName;
+ RxString get searchAccessibleItemNameText =>
+ gFFI.groupModel.searchAccessibleItemNameText;
static TextEditingController searchUserController = TextEditingController();
@override
@@ -72,7 +75,7 @@ class _MyGroupState extends State {
child: Container(
width: double.infinity,
height: double.infinity,
- child: _buildUserContacts(),
+ child: _buildLeftList(),
),
)
],
@@ -105,7 +108,7 @@ class _MyGroupState extends State {
_buildLeftHeader(),
Container(
width: double.infinity,
- child: _buildUserContacts(),
+ child: _buildLeftList(),
)
],
),
@@ -130,7 +133,8 @@ class _MyGroupState extends State {
child: TextField(
controller: searchUserController,
onChanged: (value) {
- searchUserText.value = value;
+ searchAccessibleItemNameText.value = value;
+ selectedAccessibleItemName.value = '';
},
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
@@ -145,25 +149,35 @@ class _MyGroupState extends State {
border: InputBorder.none,
isDense: true,
),
- )),
+ ).workaroundFreezeLinuxMint()),
],
);
}
- Widget _buildUserContacts() {
+ Widget _buildLeftList() {
return Obx(() {
- final items = gFFI.groupModel.users.where((p0) {
- if (searchUserText.isNotEmpty) {
+ final userItems = gFFI.groupModel.users.where((p0) {
+ if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.toLowerCase()
- .contains(searchUserText.value.toLowerCase());
+ .contains(searchAccessibleItemNameText.value.toLowerCase());
+ }
+ return true;
+ }).toList();
+ final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
+ if (searchAccessibleItemNameText.isNotEmpty) {
+ return p0.name
+ .toLowerCase()
+ .contains(searchAccessibleItemNameText.value.toLowerCase());
}
return true;
}).toList();
listView(bool isPortrait) => ListView.builder(
shrinkWrap: isPortrait,
- itemCount: items.length,
- itemBuilder: (context, index) => _buildUserItem(items[index]));
+ itemCount: deviceGroupItems.length + userItems.length,
+ itemBuilder: (context, index) => index < deviceGroupItems.length
+ ? _buildDeviceGroupItem(deviceGroupItems[index])
+ : _buildUserItem(userItems[index - deviceGroupItems.length]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return Obx(() => stateGlobal.isPortrait.isFalse
? listView(false)
@@ -174,14 +188,16 @@ class _MyGroupState extends State {
Widget _buildUserItem(UserPayload user) {
final username = user.name;
return InkWell(onTap: () {
- if (selectedUser.value != username) {
- selectedUser.value = username;
+ isSelectedDeviceGroup.value = false;
+ if (selectedAccessibleItemName.value != username) {
+ selectedAccessibleItemName.value = username;
} else {
- selectedUser.value = '';
+ selectedAccessibleItemName.value = '';
}
}, child: Obx(
() {
- bool selected = selectedUser.value == username;
+ bool selected = !isSelectedDeviceGroup.value &&
+ selectedAccessibleItemName.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container(
@@ -238,4 +254,43 @@ class _MyGroupState extends State {
},
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
}
+
+ Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) {
+ final name = deviceGroup.name;
+ return InkWell(onTap: () {
+ isSelectedDeviceGroup.value = true;
+ if (selectedAccessibleItemName.value != name) {
+ selectedAccessibleItemName.value = name;
+ } else {
+ selectedAccessibleItemName.value = '';
+ }
+ }, child: Obx(
+ () {
+ bool selected = isSelectedDeviceGroup.value &&
+ selectedAccessibleItemName.value == name;
+ return Container(
+ decoration: BoxDecoration(
+ color: selected ? MyTheme.color(context).highlight : null,
+ border: Border(
+ bottom: BorderSide(
+ width: 0.7,
+ color: Theme.of(context).dividerColor.withOpacity(0.1))),
+ ),
+ child: Container(
+ child: Row(
+ children: [
+ Container(
+ width: 20,
+ height: 20,
+ child: Icon(IconFont.deviceGroupOutline,
+ color: MyTheme.accent, size: 19),
+ ).marginOnly(right: 4),
+ Expanded(child: Text(name)),
+ ],
+ ).paddingSymmetric(vertical: 4),
+ ),
+ );
+ },
+ )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
+ }
}
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index 0a15eb45b88..db9f7af008f 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -488,8 +488,11 @@ abstract class BasePeerCard extends StatelessWidget {
BuildContext context,
String title, {
bool isFileTransfer = false,
+ bool isViewCamera = false,
bool isTcpTunneling = false,
bool isRDP = false,
+ bool isTerminal = false,
+ bool isTerminalRunAsAdmin = false,
}) {
return MenuEntryButton(
childBuilder: (TextStyle? style) => Text(
@@ -497,13 +500,18 @@ abstract class BasePeerCard extends StatelessWidget {
style: style,
),
proc: () {
+ if (isTerminalRunAsAdmin) {
+ setEnvTerminalAdmin();
+ }
connectInPeerTab(
context,
peer,
tab,
isFileTransfer: isFileTransfer,
+ isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
+ isTerminal: isTerminal || isTerminalRunAsAdmin,
);
},
padding: menuPadding,
@@ -530,6 +538,33 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
+ @protected
+ MenuEntryBase _viewCameraAction(BuildContext context) {
+ return _connectCommonAction(
+ context,
+ translate('View camera'),
+ isViewCamera: true,
+ );
+ }
+
+ @protected
+ MenuEntryBase _terminalAction(BuildContext context) {
+ return _connectCommonAction(
+ context,
+ '${translate('Terminal')} (beta)',
+ isTerminal: true,
+ );
+ }
+
+ @protected
+ MenuEntryBase _terminalRunAsAdminAction(BuildContext context) {
+ return _connectCommonAction(
+ context,
+ '${translate('Terminal (Run as administrator)')} (beta)',
+ isTerminalRunAsAdmin: true,
+ );
+ }
+
@protected
MenuEntryBase _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction(
@@ -716,18 +751,18 @@ abstract class BasePeerCard extends StatelessWidget {
switch (tab) {
case PeerTabIndex.recent:
await bind.mainRemovePeer(id: id);
- await bind.mainLoadRecentPeers();
+ bind.mainLoadRecentPeers();
break;
case PeerTabIndex.fav:
final favs = (await bind.mainGetFav()).toList();
if (favs.remove(id)) {
await bind.mainStoreFav(favs: favs);
- await bind.mainLoadFavPeers();
+ bind.mainLoadFavPeers();
}
break;
case PeerTabIndex.lan:
await bind.mainRemoveDiscovered(id: id);
- await bind.mainLoadLanPeers();
+ bind.mainLoadLanPeers();
break;
case PeerTabIndex.ab:
await gFFI.abModel.deletePeers([id]);
@@ -880,8 +915,14 @@ class RecentPeerCard extends BasePeerCard {
final List> menuItems = [
_connectAction(context),
_transferFileAction(context),
+ _viewCameraAction(context),
+ _terminalAction(context),
];
+ if (peer.platform == kPeerPlatformWindows) {
+ menuItems.add(_terminalRunAsAdminAction(context));
+ }
+
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -939,7 +980,14 @@ class FavoritePeerCard extends BasePeerCard {
final List> menuItems = [
_connectAction(context),
_transferFileAction(context),
+ _viewCameraAction(context),
+ _terminalAction(context),
];
+
+ if (peer.platform == kPeerPlatformWindows) {
+ menuItems.add(_terminalRunAsAdminAction(context));
+ }
+
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
@@ -992,8 +1040,14 @@ class DiscoveredPeerCard extends BasePeerCard {
final List> menuItems = [
_connectAction(context),
_transferFileAction(context),
+ _viewCameraAction(context),
+ _terminalAction(context),
];
+ if (peer.platform == kPeerPlatformWindows) {
+ menuItems.add(_terminalRunAsAdminAction(context));
+ }
+
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -1045,12 +1099,21 @@ class AddressBookPeerCard extends BasePeerCard {
final List> menuItems = [
_connectAction(context),
_transferFileAction(context),
+ _viewCameraAction(context),
+ _terminalAction(context),
];
+
+ if (peer.platform == kPeerPlatformWindows) {
+ menuItems.add(_terminalRunAsAdminAction(context));
+ }
+
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
- // menuItems.add(await _forceAlwaysRelayAction(peer.id));
+ if (!isWeb) {
+ menuItems.add(await _forceAlwaysRelayAction(peer.id));
+ }
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
@@ -1177,12 +1240,21 @@ class MyGroupPeerCard extends BasePeerCard {
final List> menuItems = [
_connectAction(context),
_transferFileAction(context),
+ _viewCameraAction(context),
+ _terminalAction(context),
];
+
+ if (peer.platform == kPeerPlatformWindows) {
+ menuItems.add(_terminalRunAsAdminAction(context));
+ }
+
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
- // menuItems.add(await _forceAlwaysRelayAction(peer.id));
+ if (!isWeb) {
+ menuItems.add(await _forceAlwaysRelayAction(peer.id));
+ }
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
@@ -1257,7 +1329,7 @@ void _rdpDialog(String id) async {
hintText: '3389'),
controller: portController,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
@@ -1277,7 +1349,7 @@ void _rdpDialog(String id) async {
labelText:
isDesktop ? null : translate('Username')),
controller: userController,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
@@ -1305,7 +1377,7 @@ void _rdpDialog(String id) async {
? Icons.visibility_off
: Icons.visibility))),
controller: passwordController,
- )),
+ ).workaroundFreezeLinuxMint()),
),
],
))
@@ -1398,8 +1470,10 @@ class TagPainter extends CustomPainter {
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false,
+ bool isViewCamera = false,
bool isTcpTunneling = false,
- bool isRDP = false}) async {
+ bool isRDP = false,
+ bool isTerminal = false}) async {
var password = '';
bool isSharedPassword = false;
if (tab == PeerTabIndex.ab) {
@@ -1423,6 +1497,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
password: password,
isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer,
+ isTerminal: isTerminal,
+ isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP);
}
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index 35975078805..4849f278327 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -33,8 +33,8 @@ class PeerTabPage extends StatefulWidget {
class _TabEntry {
final Widget widget;
- final Function({dynamic hint}) load;
- _TabEntry(this.widget, this.load);
+ final Function({dynamic hint})? load;
+ _TabEntry(this.widget, [this.load]);
}
EdgeInsets? _menuPadding() {
@@ -44,21 +44,15 @@ EdgeInsets? _menuPadding() {
class _PeerTabPageState extends State
with SingleTickerProviderStateMixin {
final List<_TabEntry> entries = [
- _TabEntry(
- RecentPeersView(
- menuPadding: _menuPadding(),
- ),
- bind.mainLoadRecentPeers),
- _TabEntry(
- FavoritePeersView(
- menuPadding: _menuPadding(),
- ),
- bind.mainLoadFavPeers),
- _TabEntry(
- DiscoveredPeersView(
- menuPadding: _menuPadding(),
- ),
- bind.mainDiscover),
+ _TabEntry(RecentPeersView(
+ menuPadding: _menuPadding(),
+ )),
+ _TabEntry(FavoritePeersView(
+ menuPadding: _menuPadding(),
+ )),
+ _TabEntry(DiscoveredPeersView(
+ menuPadding: _menuPadding(),
+ )),
_TabEntry(
AddressBook(
menuPadding: _menuPadding(),
@@ -100,7 +94,7 @@ class _PeerTabPageState extends State
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
}
gFFI.peerTabModel.setCurrentTab(tabIndex);
- entries[tabIndex].load(hint: false);
+ entries[tabIndex].load?.call(hint: false);
}
}
@@ -225,7 +219,7 @@ class _PeerTabPageState extends State
child: RefreshWidget(
onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) {
- entries[gFFI.peerTabModel.currentTab].load();
+ entries[gFFI.peerTabModel.currentTab].load?.call();
}
},
spinning: loading,
@@ -404,7 +398,7 @@ class _PeerTabPageState extends State
for (var p in peers) {
await bind.mainRemovePeer(id: p.id);
}
- await bind.mainLoadRecentPeers();
+ bind.mainLoadRecentPeers();
break;
case 1:
final favs = (await bind.mainGetFav()).toList();
@@ -412,13 +406,13 @@ class _PeerTabPageState extends State
favs.remove(p.id);
}).toList();
await bind.mainStoreFav(favs: favs);
- await bind.mainLoadFavPeers();
+ bind.mainLoadFavPeers();
break;
case 2:
for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id);
}
- await bind.mainLoadLanPeers();
+ bind.mainLoadLanPeers();
break;
case 3:
await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
@@ -743,7 +737,7 @@ class _PeerSearchBarState extends State {
border: InputBorder.none,
isDense: true,
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
// Icon(Icons.close),
IconButton(
diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart
index 3e34f882d1d..94f4af035a6 100644
--- a/flutter/lib/common/widgets/peers_view.dart
+++ b/flutter/lib/common/widgets/peers_view.dart
@@ -25,13 +25,13 @@ class PeerSortType {
static const String remoteId = 'Remote ID';
static const String remoteHost = 'Remote Host';
static const String username = 'Username';
- // static const String status = 'Status';
+ static const String status = 'Status';
static List values = [
PeerSortType.remoteId,
PeerSortType.remoteHost,
PeerSortType.username,
- // PeerSortType.status
+ PeerSortType.status
];
}
@@ -384,9 +384,9 @@ class _PeersViewState extends State<_PeersView>
peers.sort((p1, p2) =>
p1.username.toLowerCase().compareTo(p2.username.toLowerCase()));
break;
- // case PeerSortType.status:
- // peers.sort((p1, p2) => p1.online ? -1 : 1);
- // break;
+ case PeerSortType.status:
+ peers.sort((p1, p2) => p1.online ? -1 : 1);
+ break;
}
}
@@ -501,6 +501,7 @@ class DiscoveredPeersView extends BasePeersView {
Widget build(BuildContext context) {
final widget = super.build(context);
bind.mainLoadLanPeers();
+ bind.mainDiscover();
return widget;
}
}
@@ -562,14 +563,26 @@ class MyGroupPeerView extends BasePeersView {
);
static bool filter(Peer peer) {
- if (gFFI.groupModel.searchUserText.isNotEmpty) {
- if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
+ final model = gFFI.groupModel;
+ if (model.searchAccessibleItemNameText.isNotEmpty) {
+ final text = model.searchAccessibleItemNameText.value;
+ final searchPeersOfUser = peer.loginName.contains(text) &&
+ model.users.any((user) => user.name == peer.loginName);
+ final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
+ model.deviceGroups.any((g) => g.name == peer.device_group_name);
+ if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
return false;
}
}
- if (gFFI.groupModel.selectedUser.isNotEmpty) {
- if (gFFI.groupModel.selectedUser.value != peer.loginName) {
- return false;
+ if (model.selectedAccessibleItemName.isNotEmpty) {
+ if (model.isSelectedDeviceGroup.value) {
+ if (model.selectedAccessibleItemName.value != peer.device_group_name) {
+ return false;
+ }
+ } else {
+ if (model.selectedAccessibleItemName.value != peer.loginName) {
+ return false;
+ }
}
}
return true;
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index c31350b047c..8eb0ecbc355 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -1,4 +1,5 @@
import 'dart:convert';
+import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget {
class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child;
final FFI ffi;
-
+ final bool isCamera;
late final InputModel inputModel = ffi.inputModel;
late final FfiModel ffiModel = ffi.ffiModel;
RawTouchGestureDetectorRegion({
required this.child,
required this.ffi,
+ this.isCamera = false,
});
@override
@@ -109,9 +111,13 @@ class _RawTouchGestureDetectorRegionState
);
}
+ bool isNotTouchBasedDevice() {
+ return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
+ }
+
onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -124,7 +130,7 @@ class _RawTouchGestureDetectorRegionState
onTapUp(TapUpDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -140,7 +146,7 @@ class _RawTouchGestureDetectorRegionState
}
onTap() async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (!handleTouch) {
@@ -151,7 +157,7 @@ class _RawTouchGestureDetectorRegionState
onDoubleTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -161,7 +167,7 @@ class _RawTouchGestureDetectorRegionState
}
onDoubleTap() async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
@@ -177,7 +183,7 @@ class _RawTouchGestureDetectorRegionState
onLongPressDown(LongPressDownDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -187,11 +193,16 @@ class _RawTouchGestureDetectorRegionState
return;
}
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
+ if (ffiModel.isPeerMobile) {
+ await ffi.cursorModel
+ .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+ await inputModel.tapDown(MouseButtons.left);
+ }
}
}
onLongPressUp() async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -201,24 +212,40 @@ class _RawTouchGestureDetectorRegionState
// for mobiles
onLongPress() async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
+ return;
+ }
+ if (!ffi.ffiModel.isPeerMobile) {
+ if (handleTouch) {
+ final isMoved = await ffi.cursorModel
+ .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+ if (!isMoved) {
+ return;
+ }
+ }
+ await inputModel.tap(MouseButtons.right);
+ } else {
+ // It's better to send a message to tell the controlled device that the long press event is triggered.
+ // We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event.
+ // It's not accurate and it's better to use the same detection logic in the controlling side.
+ }
+ }
+
+ onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
+ if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
- final isMoved = await ffi.cursorModel
- .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
- if (!isMoved) {
+ if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
- }
- if (!ffi.ffiModel.isPeerMobile) {
- await inputModel.tap(MouseButtons.right);
+ await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
}
}
onDoubleFinerTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
_doubleFinerTapPosition = d.localPosition;
@@ -227,7 +254,7 @@ class _RawTouchGestureDetectorRegionState
onDoubleFinerTap(TapDownDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
@@ -243,7 +270,7 @@ class _RawTouchGestureDetectorRegionState
onHoldDragStart(DragStartDetails d) async {
lastDeviceKind = d.kind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (!handleTouch) {
@@ -252,7 +279,7 @@ class _RawTouchGestureDetectorRegionState
}
onHoldDragUpdate(DragUpdateDetails d) async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (!handleTouch) {
@@ -261,7 +288,7 @@ class _RawTouchGestureDetectorRegionState
}
onHoldDragEnd(DragEndDetails d) async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (!handleTouch) {
@@ -273,7 +300,7 @@ class _RawTouchGestureDetectorRegionState
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
lastDeviceKind = d.kind ?? lastDeviceKind;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (handleTouch) {
@@ -319,7 +346,7 @@ class _RawTouchGestureDetectorRegionState
}
onOneFingerPanUpdate(DragUpdateDetails d) async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
@@ -333,25 +360,27 @@ class _RawTouchGestureDetectorRegionState
onOneFingerPanEnd(DragEndDetails d) async {
_touchModePanStarted = false;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
- await inputModel.sendMouse('up', MouseButtons.left);
+ if (handleTouch) {
+ await inputModel.sendMouse('up', MouseButtons.left);
+ }
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
}
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if ((isDesktop || isWebDesktop)) {
@@ -359,6 +388,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale;
if (scale != 0) {
+ if (widget.isCamera) return;
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
@@ -375,10 +405,11 @@ class _RawTouchGestureDetectorRegionState
}
onTwoFingerScaleEnd(ScaleEndDetails d) async {
- if (lastDeviceKind != PointerDeviceKind.touch) {
+ if (isNotTouchBasedDevice()) {
return;
}
if ((isDesktop || isWebDesktop)) {
+ if (widget.isCamera) return;
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
@@ -430,7 +461,8 @@ class _RawTouchGestureDetectorRegionState
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
- ..onLongPress = onLongPress;
+ ..onLongPress = onLongPress
+ ..onLongPressMoveUpdate = onLongPressMoveUpdate;
}),
// Customized
HoldTapMoveGestureRecognizer:
@@ -512,3 +544,46 @@ class RawPointerMouseRegion extends StatelessWidget {
);
}
}
+
+class CameraRawPointerMouseRegion extends StatelessWidget {
+ final InputModel inputModel;
+ final Widget child;
+ final PointerEnterEventListener? onEnter;
+ final PointerExitEventListener? onExit;
+ final PointerDownEventListener? onPointerDown;
+ final PointerUpEventListener? onPointerUp;
+
+ CameraRawPointerMouseRegion({
+ this.onEnter,
+ this.onExit,
+ this.onPointerDown,
+ this.onPointerUp,
+ required this.inputModel,
+ required this.child,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ onPointerHover: (evt) {
+ final offset = evt.position;
+ double x = offset.dx;
+ double y = max(0.0, offset.dy);
+ inputModel.handlePointerDevicePos(
+ kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault);
+ },
+ onPointerDown: (evt) {
+ onPointerDown?.call(evt);
+ },
+ onPointerUp: (evt) {
+ onPointerUp?.call(evt);
+ },
+ child: MouseRegion(
+ cursor: MouseCursor.defer,
+ onEnter: onEnter,
+ onExit: onExit,
+ child: child,
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart
index 5bcb73a4c5e..b57657274a2 100644
--- a/flutter/lib/common/widgets/setting_widgets.dart
+++ b/flutter/lib/common/widgets/setting_widgets.dart
@@ -243,8 +243,99 @@ List<(String, String)> otherDefaultSettings() {
(
'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession
- )
+ ),
+ ('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
];
return v;
}
+
+class TrackpadSpeedWidget extends StatefulWidget {
+ final SimpleWrapper value;
+ // If null, no debouncer will be applied.
+ final Function(int)? onDebouncer;
+
+ TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer});
+
+ @override
+ TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
+}
+
+class TrackpadSpeedWidgetState extends State {
+ final TextEditingController _controller = TextEditingController();
+ late final Debouncer debouncerSpeed;
+
+ set value(int v) => widget.value.value = v;
+ int get value => widget.value.value;
+
+ void updateValue(int newValue) {
+ setState(() {
+ value = newValue.clamp(kMinTrackpadSpeed, kMaxTrackpadSpeed);
+ // Scale the trackpad speed value to a percentage for display purposes.
+ _controller.text = value.toString();
+ if (widget.onDebouncer != null) {
+ debouncerSpeed.setValue(value);
+ }
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ debouncerSpeed = Debouncer(
+ Duration(milliseconds: 1000),
+ onChanged: widget.onDebouncer,
+ initialValue: widget.value.value,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_controller.text.isEmpty) {
+ _controller.text = value.toString();
+ }
+ return Row(
+ children: [
+ Expanded(
+ flex: 3,
+ child: Slider(
+ value: value.toDouble(),
+ min: kMinTrackpadSpeed.toDouble(),
+ max: kMaxTrackpadSpeed.toDouble(),
+ divisions: ((kMaxTrackpadSpeed - kMinTrackpadSpeed) / 10).round(),
+ onChanged: (double v) => updateValue(v.round()),
+ ),
+ ),
+ Expanded(
+ flex: 1,
+ child: Row(
+ children: [
+ SizedBox(
+ width: 56,
+ child: TextField(
+ controller: _controller,
+ keyboardType: TextInputType.number,
+ textAlign: TextAlign.center,
+ onSubmitted: (text) {
+ int? v = int.tryParse(text);
+ if (v != null) {
+ updateValue(v);
+ }
+ },
+ style: const TextStyle(fontSize: 13),
+ decoration: InputDecoration(
+ contentPadding:
+ EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
+ ),
+ ),
+ ).marginOnly(right: 8.0),
+ Text(
+ '%',
+ style: const TextStyle(fontSize: 15),
+ )
+ ],
+ )),
+ ],
+ );
+ }
+}
diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart
index 153121057e5..cf5ed5c97bb 100644
--- a/flutter/lib/common/widgets/toolbar.dart
+++ b/flutter/lib/common/widgets/toolbar.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@@ -15,7 +16,7 @@ bool isEditOsPassword = false;
class TTextMenu {
final Widget child;
- final VoidCallback onPressed;
+ final VoidCallback? onPressed;
Widget? trailingIcon;
bool divider;
TTextMenu(
@@ -89,10 +90,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
+ final isDefaultConn = ffi.connType == ConnType.defaultConn;
List v = [];
// elevation
- if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
+ if (isDefaultConn &&
+ perms['keyboard'] != false &&
+ ffi.elevationModel.showRequestMenu) {
v.add(
TTextMenu(
child: Text(translate('Request Elevation')),
@@ -101,7 +105,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// osAccount / osPassword
- if (perms['keyboard'] != false) {
+ if (isDefaultConn && perms['keyboard'] != false) {
v.add(
TTextMenu(
child: Row(children: [
@@ -130,7 +134,9 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// paste
- if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
+ if (isDefaultConn &&
+ pi.platform != kPeerPlatformAndroid &&
+ perms['keyboard'] != false) {
v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
@@ -142,43 +148,55 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
}));
}
// reset canvas
- if (isMobile) {
+ if (isDefaultConn && isMobile) {
v.add(TTextMenu(
child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset()));
}
+ // https://github.com/rustdesk/rustdesk/pull/9731
+ // Does not work for connection established by "accept".
connectWithToken(
- {required bool isFileTransfer, required bool isTcpTunneling}) {
+ {bool isFileTransfer = false,
+ bool isViewCamera = false,
+ bool isTcpTunneling = false,
+ bool isTerminal = false}) {
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
connect(context, id,
isFileTransfer: isFileTransfer,
+ isViewCamera: isViewCamera,
+ isTerminal: isTerminal,
isTcpTunneling: isTcpTunneling,
connToken: connToken);
}
- // transferFile
- if (isDesktop) {
+ if (isDefaultConn && isDesktop) {
v.add(
TTextMenu(
child: Text(translate('Transfer file')),
- onPressed: () =>
- connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
+ onPressed: () => connectWithToken(isFileTransfer: true)),
+ );
+ v.add(
+ TTextMenu(
+ child: Text(translate('View camera')),
+ onPressed: () => connectWithToken(isViewCamera: true)),
+ );
+ v.add(
+ TTextMenu(
+ child: Text('${translate('Terminal')} (beta)'),
+ onPressed: () => connectWithToken(isTerminal: true)),
);
- }
- // tcpTunneling
- if (isDesktop) {
v.add(
TTextMenu(
child: Text(translate('TCP tunneling')),
- onPressed: () =>
- connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
+ onPressed: () => connectWithToken(isTcpTunneling: true)),
);
}
// note
- if (bind
- .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
- .isNotEmpty) {
+ if (isDefaultConn &&
+ bind
+ .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
+ .isNotEmpty) {
v.add(
TTextMenu(
child: Text(translate('Note')),
@@ -186,11 +204,12 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// divider
- if (isDesktop || isWebDesktop) {
+ if (isDefaultConn && (isDesktop || isWebDesktop)) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
}
// ctrlAltDel
- if (!ffiModel.viewOnly &&
+ if (isDefaultConn &&
+ !ffiModel.viewOnly &&
ffiModel.keyboard &&
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
v.add(
@@ -200,7 +219,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// restart
- if (perms['restart'] != false &&
+ if (isDefaultConn &&
+ perms['restart'] != false &&
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
@@ -212,7 +232,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// insertLock
- if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
+ if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
v.add(
TTextMenu(
child: Text(translate('Insert Lock')),
@@ -220,7 +240,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// blockUserInput
- if (ffi.ffiModel.keyboard &&
+ if (isDefaultConn &&
+ ffi.ffiModel.keyboard &&
ffi.ffiModel.permissions['block_input'] != false &&
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
{
@@ -236,12 +257,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
}));
}
// switchSides
- if (isDesktop &&
+ if (isDefaultConn &&
+ isDesktop &&
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 &&
- bind.peerGetDefaultSessionsCount(id: id) == 1) {
+ bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu(
child: Text(translate('Switch Sides')),
onPressed: () =>
@@ -275,6 +297,41 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
),
onPressed: () => ffi.recordingModel.toggle()));
}
+
+ // to-do:
+ // 1. Web desktop
+ // 2. Mobile, copy the image to the clipboard
+ if (isDesktop) {
+ final isScreenshotSupported = bind.sessionGetCommonSync(
+ sessionId: sessionId, key: 'is_screenshot_supported', param: '');
+ if ('true' == isScreenshotSupported) {
+ v.add(TTextMenu(
+ child: Text(ffi.ffiModel.timerScreenshot != null
+ ? '${translate('Taking screenshot')} ...'
+ : translate('Take screenshot')),
+ onPressed: ffi.ffiModel.timerScreenshot != null
+ ? null
+ : () {
+ if (pi.currentDisplay == kAllDisplayValue) {
+ msgBox(
+ sessionId,
+ 'custom-nook-nocancel-hasclose-info',
+ 'Take screenshot',
+ 'screenshot-merged-screen-not-supported-tip',
+ '',
+ ffi.dialogManager);
+ } else {
+ bind.sessionTakeScreenshot(
+ sessionId: sessionId, display: pi.currentDisplay);
+ ffi.ffiModel.timerScreenshot =
+ Timer(Duration(seconds: 30), () {
+ ffi.ffiModel.timerScreenshot = null;
+ });
+ }
+ },
+ ));
+ }
+ }
// fingerprint
if (!(isDesktop || isWebDesktop)) {
v.add(TTextMenu(
@@ -523,6 +580,7 @@ Future> toolbarDisplayToggle(
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
+ final isDefaultConn = ffi.connType == ConnType.defaultConn;
// show quality monitor
final option = 'show-quality-monitor';
@@ -535,7 +593,7 @@ Future> toolbarDisplayToggle(
},
child: Text(translate('Show quality monitor'))));
// mute
- if (perms['audio'] != false) {
+ if (isDefaultConn && perms['audio'] != false) {
final option = 'disable-audio';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
@@ -556,7 +614,8 @@ Future> toolbarDisplayToggle(
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
- if (ffiModel.keyboard &&
+ if (isDefaultConn &&
+ ffiModel.keyboard &&
perms['file'] != false &&
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
final enabled = !ffiModel.viewOnly;
@@ -574,7 +633,7 @@ Future> toolbarDisplayToggle(
child: Text(translate('Enable file copy and paste'))));
}
// disable clipboard
- if (ffiModel.keyboard && perms['clipboard'] != false) {
+ if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) {
final enabled = !ffiModel.viewOnly;
final option = 'disable-clipboard';
var value =
@@ -591,7 +650,7 @@ Future> toolbarDisplayToggle(
child: Text(translate('Disable clipboard'))));
}
// lock after session end
- if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
+ if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) {
final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end';
final value =
@@ -656,12 +715,12 @@ Future> toolbarDisplayToggle(
child: Text(translate('True color (4:4:4)'))));
}
- if (isMobile) {
+ if (isDefaultConn && isMobile) {
v.addAll(toolbarKeyboardToggles(ffi));
}
// view mode (mobile only, desktop is in keyboard menu)
- if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
+ if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 95b207826a7..eda0e11cff4 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -44,7 +45,9 @@ const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
+const String kAppTypeDesktopViewCamera = "view camera";
const String kAppTypeDesktopPortForward = "port forward";
+const String kAppTypeDesktopTerminal = "terminal";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
@@ -58,7 +61,10 @@ const String kWindowConnect = "connect";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer";
+const String kWindowEventNewViewCamera = "new_view_camera";
const String kWindowEventNewPortForward = "new_port_forward";
+const String kWindowEventNewTerminal = "new_terminal";
+const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions";
const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list";
@@ -75,6 +81,7 @@ const String kOptionScrollStyle = "scroll_style";
const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render";
+const String kOptionD3DRender = "allow-d3d-render";
const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows";
const String kOptionForceAlwaysRelay = "force-always-relay";
@@ -94,9 +101,13 @@ const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";
// "Settings -> Security -> Permissions"
+const String kOptionEnableRemotePrinter = "enable-remote-printer";
const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio";
+const String kOptionEnableCamera = "enable-camera";
+const String kOptionEnableTerminal = "enable-terminal";
+const String kOptionTerminalPersistent = "terminal-persistent";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
@@ -104,6 +115,8 @@ const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method";
const String kOptionApproveMode = "approve-mode";
+const String kOptionAllowNumericOneTimePassword =
+ "allow-numeric-one-time-password";
const String kOptionCollapseToolbar = "collapse_toolbar";
const String kOptionShowRemoteCursor = "show_remote_cursor";
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
@@ -133,16 +146,24 @@ const String kOptionCurrentAbName = "current-ab-name";
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
+const String kOptionAllowAutoUpdate = "allow-auto-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
+const String kOptionEnableUdpPunch = "enable-udp-punch";
+const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
+// network options
+const String kOptionAllowWebSocket = "allow-websocket";
+
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
+const String kOptionHideWebSocketSetting = "hide-websocket-settings";
+const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
@@ -214,6 +235,21 @@ const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
+// trackpad speed
+const String kKeyTrackpadSpeed = 'trackpad-speed';
+const int kMinTrackpadSpeed = 10;
+const int kDefaultTrackpadSpeed = 100;
+const int kMaxTrackpadSpeed = 1000;
+
+// incomming (should be incoming) is kept, because change it will break the previous setting.
+const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
+const String kValuePrinterIncomingJobDismiss = 'dismiss';
+const String kValuePrinterIncomingJobDefault = '';
+const String kValuePrinterIncomingJobSelected = 'selected';
+const String kKeyPrinterSelected = 'printer-selected-name';
+const String kKeyPrinterSave = 'allow-printer-dialog-save';
+const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print';
+
double kNewWindowOffset = isWindows
? 56.0
: isLinux
@@ -248,7 +284,7 @@ const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
const kWindowResizeEdgeSize = 5.0;
-const kWindowBorderWidth = 1.0;
+final kWindowBorderWidth = isWindows ? 0.0 : 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;
@@ -302,6 +338,12 @@ const kRemoteImageQualityCustom = 'custom';
const kIgnoreDpi = true;
+const Set kTouchBasedDeviceKinds = {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.stylus,
+ PointerDeviceKind.invertedStylus,
+};
+
// ================================ mobile ================================
// Magic numbers, maybe need to avoid it or use a better way to get them.
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index f2c7121016e..2f244011ee5 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -2,10 +2,12 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -17,7 +19,7 @@ import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart';
-import '../widgets/button.dart';
+import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
class OnlineStatusWidget extends StatefulWidget {
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
@@ -39,7 +41,7 @@ class _OnlineStatusWidgetState extends State {
double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() {
- const url = "https://rustdesk.com/pricing.html";
+ const url = "https://rustdesk.com/pricing";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
@@ -200,18 +202,25 @@ class _ConnectionPageState extends State
final _idController = IDTextEditingController();
final RxBool _idInputFocused = false.obs;
+ final FocusNode _idFocusNode = FocusNode();
+ final TextEditingController _idEditingController = TextEditingController();
+
+ String selectedConnectionType = 'Connect';
bool isWindowMinimized = false;
- List peers = [];
- bool isPeersLoading = false;
- bool isPeersLoaded = false;
+ final AllPeersLoader _allPeersLoader = AllPeersLoader();
+
// https://github.com/flutter/flutter/issues/157244
Iterable _autocompleteOpts = [];
+ final _menuOpen = false.obs;
+
@override
void initState() {
super.initState();
+ _allPeersLoader.init(setState);
+ _idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@@ -222,6 +231,7 @@ class _ConnectionPageState extends State
}
});
}
+ Get.put(_idEditingController);
Get.put(_idController);
windowManager.addListener(this);
}
@@ -230,6 +240,10 @@ class _ConnectionPageState extends State
void dispose() {
_idController.dispose();
windowManager.removeListener(this);
+ _allPeersLoader.clear();
+ _idFocusNode.removeListener(onFocusChanged);
+ _idFocusNode.dispose();
+ _idEditingController.dispose();
if (Get.isRegistered()) {
Get.delete();
}
@@ -273,6 +287,20 @@ class _ConnectionPageState extends State
bind.mainOnMainWindowClose();
}
+ void onFocusChanged() {
+ _idInputFocused.value = _idFocusNode.hasFocus;
+ if (_idFocusNode.hasFocus) {
+ if (_allPeersLoader.needLoad) {
+ _allPeersLoader.getAllPeers();
+ }
+
+ final textLength = _idEditingController.value.text.length;
+ // Select all to facilitate removing text, just following the behavior of address input of chrome.
+ _idEditingController.selection =
+ TextSelection(baseOffset: 0, extentOffset: textLength);
+ }
+ }
+
@override
Widget build(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
@@ -281,12 +309,6 @@ class _ConnectionPageState extends State
Expanded(
child: Column(
children: [
- Row(
- children: [
- Flexible(child: _buildRemoteIDTextField(context)),
- ],
- ).marginOnly(top: 22),
- SizedBox(height: 12),
Divider().paddingOnly(right: 12),
Expanded(child: PeerTabPage()),
],
@@ -299,21 +321,15 @@ class _ConnectionPageState extends State
/// Callback for the connect button.
/// Connects to the selected peer.
- void onConnect({bool isFileTransfer = false}) {
+ void onConnect(
+ {bool isFileTransfer = false,
+ bool isViewCamera = false,
+ bool isTerminal = false}) {
var id = _idController.id;
- connect(context, id, isFileTransfer: isFileTransfer);
- }
-
- Future _fetchPeers() async {
- setState(() {
- isPeersLoading = true;
- });
- await Future.delayed(Duration(milliseconds: 100));
- peers = await getAllPeers();
- setState(() {
- isPeersLoading = false;
- isPeersLoaded = true;
- });
+ connect(context, id,
+ isFileTransfer: isFileTransfer,
+ isViewCamera: isViewCamera,
+ isTerminal: isTerminal);
}
/// UI for the remote ID TextField.
@@ -332,11 +348,12 @@ class _ConnectionPageState extends State
Row(
children: [
Expanded(
- child: Autocomplete(
+ child: RawAutocomplete(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable.empty();
- } else if (peers.isEmpty && !isPeersLoaded) {
+ } else if (_allPeersLoader.peers.isEmpty &&
+ !_allPeersLoader.isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
@@ -350,6 +367,7 @@ class _ConnectionPageState extends State
rdpPort: '',
rdpUsername: '',
loginName: '',
+ device_group_name: '',
);
_autocompleteOpts = [emptyPeer];
} else {
@@ -362,7 +380,7 @@ class _ConnectionPageState extends State
);
}
String textToFind = textEditingValue.text.toLowerCase();
- _autocompleteOpts = peers
+ _autocompleteOpts = _allPeersLoader.peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -376,25 +394,16 @@ class _ConnectionPageState extends State
}
return _autocompleteOpts;
},
+ focusNode: _idFocusNode,
+ textEditingController: _idEditingController,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
- fieldTextEditingController.text = _idController.text;
- Get.put(fieldTextEditingController);
- fieldFocusNode.addListener(() async {
- _idInputFocused.value = fieldFocusNode.hasFocus;
- if (fieldFocusNode.hasFocus && !isPeersLoading) {
- _fetchPeers();
- }
- });
- final textLength =
- fieldTextEditingController.value.text.length;
- // select all to facilitate removing text, just following the behavior of address input of chrome
- fieldTextEditingController.selection =
- TextSelection(baseOffset: 0, extentOffset: textLength);
+ updateTextAndPreserveSelection(
+ fieldTextEditingController, _idController.text);
return Obx(() => TextField(
autocorrect: false,
enableSuggestions: false,
@@ -424,7 +433,7 @@ class _ConnectionPageState extends State
onSubmitted: (_) {
onConnect();
},
- ));
+ ).workaroundFreezeLinuxMint());
},
onSelected: (option) {
setState(() {
@@ -467,7 +476,8 @@ class _ConnectionPageState extends State
maxHeight: maxHeight,
maxWidth: 319,
),
- child: peers.isEmpty && isPeersLoading
+ child: _allPeersLoader.peers.isEmpty &&
+ !_allPeersLoader.isPeersLoaded
? Container(
height: 80,
child: Center(
@@ -497,21 +507,97 @@ class _ConnectionPageState extends State
),
Padding(
padding: const EdgeInsets.only(top: 13.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Button(
- isOutline: true,
- onTap: () => onConnect(isFileTransfer: true),
- text: "Transfer file",
+ child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
+ SizedBox(
+ height: 28.0,
+ child: ElevatedButton(
+ onPressed: () {
+ onConnect();
+ },
+ child: Text(translate("Connect")),
),
- const SizedBox(
- width: 17,
+ ),
+ const SizedBox(width: 8),
+ Container(
+ height: 28.0,
+ width: 28.0,
+ decoration: BoxDecoration(
+ border: Border.all(color: Theme.of(context).dividerColor),
+ borderRadius: BorderRadius.circular(8),
),
- Button(onTap: onConnect, text: "Connect"),
- ],
- ),
- )
+ child: Center(
+ child: StatefulBuilder(
+ builder: (context, setState) {
+ var offset = Offset(0, 0);
+ return Obx(() => InkWell(
+ child: _menuOpen.value
+ ? Transform.rotate(
+ angle: pi,
+ child: Icon(IconFont.more, size: 14),
+ )
+ : Icon(IconFont.more, size: 14),
+ onTapDown: (e) {
+ offset = e.globalPosition;
+ },
+ onTap: () async {
+ _menuOpen.value = true;
+ final x = offset.dx;
+ final y = offset.dy;
+ await mod_menu
+ .showMenu(
+ context: context,
+ position: RelativeRect.fromLTRB(x, y, x, y),
+ items: [
+ (
+ 'Transfer file',
+ () => onConnect(isFileTransfer: true)
+ ),
+ (
+ 'View camera',
+ () => onConnect(isViewCamera: true)
+ ),
+ (
+ '${translate('Terminal')} (beta)',
+ () => onConnect(isTerminal: true)
+ ),
+ ]
+ .map((e) => MenuEntryButton(
+ childBuilder: (TextStyle? style) =>
+ Text(
+ translate(e.$1),
+ style: style,
+ ),
+ proc: () => e.$2(),
+ padding: EdgeInsets.symmetric(
+ horizontal:
+ kDesktopMenuPadding.left),
+ dismissOnClicked: true,
+ ))
+ .map((e) => e.build(
+ context,
+ const MenuConfig(
+ commonColor: CustomPopupMenuTheme
+ .commonColor,
+ height:
+ CustomPopupMenuTheme.height,
+ dividerHeight:
+ CustomPopupMenuTheme
+ .dividerHeight)))
+ .expand((i) => i)
+ .toList(),
+ elevation: 8,
+ )
+ .then((_) {
+ _menuOpen.value = false;
+ });
+ },
+ ));
+ },
+ ),
+ ),
+ ),
+ ]),
+ ),
],
),
),
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 10f5cc4fdba..23769115919 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -12,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
+import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -22,7 +23,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
-
import '../widgets/button.dart';
class DesktopHomePage extends StatefulWidget {
@@ -134,12 +134,17 @@ class _DesktopHomePageState extends State
color: Theme.of(context).colorScheme.background,
child: Stack(
children: [
- SingleChildScrollView(
- controller: _leftPaneScrollController,
- child: Column(
- key: _childKey,
- children: children,
- ),
+ Column(
+ children: [
+ SingleChildScrollView(
+ controller: _leftPaneScrollController,
+ child: Column(
+ key: _childKey,
+ children: children,
+ ),
+ ),
+ Expanded(child: Container())
+ ],
),
if (isOutgoingOnly)
Positioned(
@@ -237,7 +242,7 @@ class _DesktopHomePageState extends State
style: TextStyle(
fontSize: 22,
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
)
],
@@ -333,7 +338,7 @@ class _DesktopHomePageState extends State
EdgeInsets.only(top: 14, bottom: 10),
),
style: TextStyle(fontSize: 15),
- ),
+ ).workaroundFreezeLinuxMint(),
),
),
if (showOneTime)
@@ -428,13 +433,23 @@ class _DesktopHomePageState extends State
updateUrl.isNotEmpty &&
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
+ final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
+ String btnText = isToUpdate ? 'Update' : 'Download';
+ GestureTapCallback onPressed = () async {
+ final Uri url = Uri.parse('https://rustdesk.com/download');
+ await launchUrl(url);
+ };
+ if (isToUpdate) {
+ onPressed = () {
+ handleUpdate(updateUrl);
+ };
+ }
return buildInstallCard(
"Status",
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
- "Click to download", () async {
- final Uri url = Uri.parse('https://rustdesk.com/download');
- await launchUrl(url);
- }, closeButton: true);
+ btnText,
+ onPressed,
+ closeButton: true);
}
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
@@ -770,6 +785,8 @@ class _DesktopHomePageState extends State
await connectMainDesktop(
call.arguments['id'],
isFileTransfer: call.arguments['isFileTransfer'],
+ isViewCamera: call.arguments['isViewCamera'],
+ isTerminal: call.arguments['isTerminal'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
password: call.arguments['password'],
@@ -784,9 +801,15 @@ class _DesktopHomePageState extends State
} catch (e) {
debugPrint("Failed to parse window id '${call.arguments}': $e");
}
- if (windowId != null) {
+ WindowType? windowType;
+ try {
+ windowType = WindowType.values.byName(args[3]);
+ } catch (e) {
+ debugPrint("Failed to parse window type '${call.arguments}': $e");
+ }
+ if (windowId != null && windowType != null) {
await rustDeskWinManager.moveTabToNewWindow(
- windowId, args[1], args[2]);
+ windowId, args[1], args[2], windowType);
}
} else if (call.method == kWindowEventOpenMonitorSession) {
final args = jsonDecode(call.arguments);
@@ -794,9 +817,10 @@ class _DesktopHomePageState extends State
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
+ final windowType = args['window_type'] as int;
final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession(
- windowId, peerId, display, displayCount, screenRect);
+ windowId, peerId, display, displayCount, screenRect, windowType);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
@@ -834,10 +858,6 @@ class _DesktopHomePageState extends State
_uniLinksSubscription?.cancel();
Get.delete(tag: 'stop-service');
_updateTimer?.cancel();
- if (!bind.isCustomClient()) {
- platformFFI.unregisterEventHandler(
- kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
- }
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -940,7 +960,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
},
maxLength: maxLength,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
),
@@ -967,7 +987,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
},
maxLength: maxLength,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
),
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index 56a99446c38..cc1f3f27194 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
@@ -55,6 +56,7 @@ enum SettingsTabKey {
display,
plugin,
account,
+ printer,
about,
}
@@ -74,6 +76,9 @@ class DesktopSettingPage extends StatefulWidget {
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
+ if (isWindows &&
+ bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
+ SettingsTabKey.printer,
SettingsTabKey.about,
];
@@ -198,6 +203,10 @@ class _DesktopSettingPageState extends State
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break;
+ case SettingsTabKey.printer:
+ settingTabs
+ .add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print));
+ break;
case SettingsTabKey.about:
settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
@@ -229,6 +238,9 @@ class _DesktopSettingPageState extends State
case SettingsTabKey.account:
children.add(const _Account());
break;
+ case SettingsTabKey.printer:
+ children.add(const _Printer());
+ break;
case SettingsTabKey.about:
children.add(const _About());
break;
@@ -460,6 +472,8 @@ class _GeneralState extends State<_General> {
}
Widget other() {
+ final showAutoUpdate =
+ isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
final children = [
if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
@@ -496,6 +510,16 @@ class _GeneralState extends State<_General> {
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
),
+ if (isWindows)
+ Tooltip(
+ message: translate('d3d_render_tip'),
+ child: _OptionCheckBox(
+ context,
+ "Use D3D rendering",
+ kOptionD3DRender,
+ isServer: false,
+ ),
+ ),
if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
@@ -503,12 +527,33 @@ class _GeneralState extends State<_General> {
kOptionEnableCheckUpdate,
isServer: false,
),
+ if (showAutoUpdate)
+ _OptionCheckBox(
+ context,
+ 'Auto update',
+ kOptionAllowAutoUpdate,
+ isServer: true,
+ ),
if (isWindows && !bind.isOutgoingOnly())
_OptionCheckBox(
context,
'Capture screen using DirectX',
kOptionDirectxCapture,
- )
+ ),
+ if (!bind.isIncomingOnly()) ...[
+ _OptionCheckBox(
+ context,
+ 'Enable UDP hole punching',
+ kOptionEnableUdpPunch,
+ isServer: false,
+ ),
+ _OptionCheckBox(
+ context,
+ 'Enable IPv6 P2P connection',
+ kOptionEnableIpv6Punch,
+ isServer: false,
+ ),
+ ],
],
];
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
@@ -607,7 +652,6 @@ class _GeneralState extends State<_General> {
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
- // canLaunchUrl blocked on windows portable, user SYSTEM
return {
'user_dir': user_dir,
'root_dir': root_dir,
@@ -954,6 +998,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(
context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
enabled: enabled, fakeValue: fakeValue),
+ if (isWindows)
+ _OptionCheckBox(
+ context, 'Enable remote printer', kOptionEnableRemotePrinter,
+ enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
@@ -961,6 +1009,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
enabled: enabled, fakeValue: fakeValue),
+ _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
+ enabled: enabled, fakeValue: fakeValue),
+ _OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal,
+ enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable TCP tunneling', kOptionEnableTunnel,
enabled: enabled, fakeValue: fakeValue),
@@ -1061,6 +1113,34 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
))
.toList();
+ final isOptFixedNumOTP =
+ isOptionFixed(kOptionAllowNumericOneTimePassword);
+ final isNumOPTChangable = !isOptFixedNumOTP && tmpEnabled && !locked;
+ final numericOneTimePassword = GestureDetector(
+ child: InkWell(
+ child: Row(
+ children: [
+ Checkbox(
+ value: model.allowNumericOneTimePassword,
+ onChanged: isNumOPTChangable
+ ? (bool? v) {
+ model.switchAllowNumericOneTimePassword();
+ }
+ : null)
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(
+ translate('Numeric one-time password'),
+ style: TextStyle(
+ color: disabledTextColor(context, isNumOPTChangable)),
+ ))
+ ],
+ )),
+ onTap: isNumOPTChangable
+ ? () => model.switchAllowNumericOneTimePassword()
+ : null,
+ ).marginOnly(left: _kContentHSubMargin - 5);
+
final modeKeys = [
'password',
'click',
@@ -1097,6 +1177,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
],
),
enabled: tmpEnabled && !locked),
+ numericOneTimePassword,
if (usePassword) radios[1],
if (usePassword)
_SubButton('Set permanent password', setPasswordDialog,
@@ -1189,7 +1270,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
- ).marginOnly(right: 15),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value &&
@@ -1346,7 +1427,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
- ).marginOnly(right: 15),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed:
@@ -1441,11 +1522,69 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
+ final hideWebSocket = isWeb ||
+ bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y';
- if (hideServer && hideProxy) {
+ if (hideServer && hideProxy && hideWebSocket) {
return Offstage();
}
+ // Helper function to create network setting ListTiles
+ Widget listTile({
+ required IconData icon,
+ required String title,
+ VoidCallback? onTap,
+ Widget? trailing,
+ bool showTooltip = false,
+ String tooltipMessage = '',
+ }) {
+ final titleWidget = showTooltip
+ ? Row(
+ children: [
+ Tooltip(
+ waitDuration: Duration(milliseconds: 1000),
+ message: translate(tooltipMessage),
+ child: Row(
+ children: [
+ Text(
+ translate(title),
+ style: TextStyle(fontSize: _kContentFontSize),
+ ),
+ SizedBox(width: 5),
+ Icon(
+ Icons.help_outline,
+ size: 14,
+ color: Theme.of(context)
+ .textTheme
+ .titleLarge
+ ?.color
+ ?.withOpacity(0.7),
+ ),
+ ],
+ ),
+ ),
+ ],
+ )
+ : Text(
+ translate(title),
+ style: TextStyle(fontSize: _kContentFontSize),
+ );
+
+ return ListTile(
+ leading: Icon(icon, color: _accentColor),
+ title: titleWidget,
+ enabled: !locked,
+ onTap: onTap,
+ trailing: trailing,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ contentPadding: EdgeInsets.symmetric(horizontal: 16),
+ minLeadingWidth: 0,
+ horizontalTitleGap: 10,
+ );
+ }
+
return _Card(
title: 'Network',
children: [
@@ -1454,39 +1593,36 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!hideServer)
- ListTile(
- leading: Icon(Icons.dns_outlined, color: _accentColor),
- title: Text(
- translate('ID/Relay Server'),
- style: TextStyle(fontSize: _kContentFontSize),
- ),
- enabled: !locked,
+ listTile(
+ icon: Icons.dns_outlined,
+ title: 'ID/Relay Server',
onTap: () => showServerSettings(gFFI.dialogManager),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10),
- ),
- contentPadding: EdgeInsets.symmetric(horizontal: 16),
- minLeadingWidth: 0,
- horizontalTitleGap: 10,
),
- if (!hideServer && !hideProxy)
+ if (!hideServer && (!hideProxy || !hideWebSocket))
Divider(height: 1, indent: 16, endIndent: 16),
if (!hideProxy)
- ListTile(
- leading:
- Icon(Icons.network_ping_outlined, color: _accentColor),
- title: Text(
- translate('Socks5/Http(s) Proxy'),
- style: TextStyle(fontSize: _kContentFontSize),
- ),
- enabled: !locked,
+ listTile(
+ icon: Icons.network_ping_outlined,
+ title: 'Socks5/Http(s) Proxy',
onTap: changeSocks5Proxy,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10),
+ ),
+ if (!hideProxy && !hideWebSocket)
+ Divider(height: 1, indent: 16, endIndent: 16),
+ if (!hideWebSocket)
+ listTile(
+ icon: Icons.web_asset_outlined,
+ title: 'Use WebSocket',
+ showTooltip: true,
+ tooltipMessage: 'websocket_tip',
+ trailing: Switch(
+ value: mainGetBoolOptionSync(kOptionAllowWebSocket),
+ onChanged: locked
+ ? null
+ : (value) {
+ mainSetBoolOption(kOptionAllowWebSocket, value);
+ setState(() {});
+ },
),
- contentPadding: EdgeInsets.symmetric(horizontal: 16),
- minLeadingWidth: 0,
- horizontalTitleGap: 10,
),
],
),
@@ -1512,6 +1648,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context),
imageQuality(context),
codec(context),
+ if (isDesktop) trackpadSpeed(context),
if (!isWeb) privacyModeImpl(context),
other(context),
]).marginOnly(bottom: _kListViewBottomMargin);
@@ -1599,6 +1736,26 @@ class _DisplayState extends State<_Display> {
]);
}
+ Widget trackpadSpeed(BuildContext context) {
+ final initSpeed = (int.tryParse(
+ bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
+ kDefaultTrackpadSpeed);
+ final curSpeed = SimpleWrapper(initSpeed);
+ void onDebouncer(int v) {
+ bind.mainSetUserDefaultOption(
+ key: kKeyTrackpadSpeed, value: v.toString());
+ // It's better to notify all sessions that the default speed is changed.
+ // But it may also be ok to take effect in the next connection.
+ }
+
+ return _Card(title: 'Default trackpad speed', children: [
+ TrackpadSpeedWidget(
+ value: curSpeed,
+ onDebouncer: onDebouncer,
+ ),
+ ]);
+ }
+
Widget codec(BuildContext context) {
onChanged(String value) async {
await bind.mainSetUserDefaultOption(
@@ -1870,6 +2027,153 @@ class _PluginState extends State<_Plugin> {
}
}
+class _Printer extends StatefulWidget {
+ const _Printer({super.key});
+
+ @override
+ State<_Printer> createState() => __PrinterState();
+}
+
+class __PrinterState extends State<_Printer> {
+ @override
+ Widget build(BuildContext context) {
+ final scrollController = ScrollController();
+ return ListView(controller: scrollController, children: [
+ outgoing(context),
+ incoming(context),
+ ]).marginOnly(bottom: _kListViewBottomMargin);
+ }
+
+ Widget outgoing(BuildContext context) {
+ final isSupportPrinterDriver =
+ bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true';
+
+ Widget tipOsNotSupported() {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: Text(translate('printer-os-requirement-tip')),
+ ).marginOnly(left: _kCardLeftMargin);
+ }
+
+ Widget tipClientNotInstalled() {
+ return Align(
+ alignment: Alignment.topLeft,
+ child:
+ Text(translate('printer-requires-installed-{$appName}-client-tip')),
+ ).marginOnly(left: _kCardLeftMargin);
+ }
+
+ Widget tipPrinterNotInstalled() {
+ final failedMsg = ''.obs;
+ platformFFI.registerEventHandler(
+ 'install-printer-res', 'install-printer-res', (evt) async {
+ if (evt['success'] as bool) {
+ setState(() {});
+ } else {
+ failedMsg.value = evt['msg'] as String;
+ }
+ }, replace: true);
+ return Column(children: [
+ Obx(
+ () => failedMsg.value.isNotEmpty
+ ? Offstage()
+ : Align(
+ alignment: Alignment.topLeft,
+ child: Text(translate('printer-{$appName}-not-installed-tip'))
+ .marginOnly(bottom: 10.0),
+ ),
+ ),
+ Obx(
+ () => failedMsg.value.isEmpty
+ ? Offstage()
+ : Align(
+ alignment: Alignment.topLeft,
+ child: Text(failedMsg.value,
+ style: DefaultTextStyle.of(context)
+ .style
+ .copyWith(color: Colors.red))
+ .marginOnly(bottom: 10.0)),
+ ),
+ _Button('Install {$appName} Printer', () {
+ failedMsg.value = '';
+ bind.mainSetCommon(key: 'install-printer', value: '');
+ })
+ ]).marginOnly(left: _kCardLeftMargin, bottom: 2.0);
+ }
+
+ Widget tipReady() {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: Text(translate('printer-{$appName}-ready-tip')),
+ ).marginOnly(left: _kCardLeftMargin);
+ }
+
+ final installed = bind.mainIsInstalled();
+ // `is-printer-installed` may fail, but it's rare case.
+ // Add additional error message here if it's really needed.
+ final isPrinterInstalled =
+ bind.mainGetCommonSync(key: 'is-printer-installed') == 'true';
+
+ final List children = [];
+ if (!isSupportPrinterDriver) {
+ children.add(tipOsNotSupported());
+ } else {
+ children.addAll([
+ if (!installed) tipClientNotInstalled(),
+ if (installed && !isPrinterInstalled) tipPrinterNotInstalled(),
+ if (installed && isPrinterInstalled) tipReady()
+ ]);
+ }
+ return _Card(title: 'Outgoing Print Jobs', children: children);
+ }
+
+ Widget incoming(BuildContext context) {
+ onRadioChanged(String value) async {
+ await bind.mainSetLocalOption(
+ key: kKeyPrinterIncomingJobAction, value: value);
+ setState(() {});
+ }
+
+ PrinterOptions printerOptions = PrinterOptions.load();
+ return _Card(title: 'Incoming Print Jobs', children: [
+ _Radio(context,
+ value: kValuePrinterIncomingJobDismiss,
+ groupValue: printerOptions.action,
+ label: 'Dismiss',
+ onChanged: onRadioChanged),
+ _Radio(context,
+ value: kValuePrinterIncomingJobDefault,
+ groupValue: printerOptions.action,
+ label: 'use-the-default-printer-tip',
+ onChanged: onRadioChanged),
+ _Radio(context,
+ value: kValuePrinterIncomingJobSelected,
+ groupValue: printerOptions.action,
+ label: 'use-the-selected-printer-tip',
+ onChanged: onRadioChanged),
+ if (printerOptions.printerNames.isNotEmpty)
+ ComboBox(
+ initialKey: printerOptions.printerName,
+ keys: printerOptions.printerNames,
+ values: printerOptions.printerNames,
+ enabled: printerOptions.action == kValuePrinterIncomingJobSelected,
+ onChanged: (value) async {
+ await bind.mainSetLocalOption(
+ key: kKeyPrinterSelected, value: value);
+ setState(() {});
+ },
+ ).marginOnly(left: 10),
+ _OptionCheckBox(
+ context,
+ 'auto-print-tip',
+ kKeyPrinterAllowAutoPrint,
+ isServer: false,
+ enabled: printerOptions.action != kValuePrinterIncomingJobDismiss,
+ )
+ ]);
+ }
+}
+
class _About extends StatefulWidget {
const _About({Key? key}) : super(key: key);
@@ -2312,7 +2616,7 @@ _LabeledTextField(
style: TextStyle(
color: disabledTextColor(context, enabled),
),
- ),
+ ).workaroundFreezeLinuxMint(),
],
),
],
@@ -2491,7 +2795,7 @@ void changeSocks5Proxy() async {
controller: proxyController,
autofocus: true,
enabled: !isOptFixed,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2511,7 +2815,7 @@ void changeSocks5Proxy() async {
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2537,7 +2841,7 @@ void changeSocks5Proxy() async {
controller: pwdController,
enabled: !isOptFixed,
maxLength: bind.mainMaxEncryptLen(),
- )),
+ ).workaroundFreezeLinuxMint()),
),
],
),
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index 90b8d7dcbf3..3f555dcaa66 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -768,7 +768,7 @@ class _FileManagerViewState extends State {
),
controller: name,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
],
),
actions: [
@@ -1657,7 +1657,7 @@ class _FileManagerViewState extends State {
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal)
: null,
- ),
+ ).workaroundFreezeLinuxMint(),
)
],
);
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index cc77cdd9581..5251498895d 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -103,11 +103,13 @@ class _FileManagerTabPageState extends State {
));
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
- : Container(
- decoration: BoxDecoration(
- border: Border.all(color: MyTheme.color(context).border!)),
- child: child,
- );
+ : workaroundWindowBorder(
+ context,
+ Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.color(context).border!)),
+ child: child,
+ ));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(
diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart
index 0ff04240b55..5bf6bafeea0 100644
--- a/flutter/lib/desktop/pages/install_page.dart
+++ b/flutter/lib/desktop/pages/install_page.dart
@@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
+ final RxBool printer = true.obs;
final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs;
@@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
+ printer.value = installOptions['PRINTER'] != '0';
}
@override
@@ -147,7 +149,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
),
- ).marginOnly(right: 10),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 10),
),
Obx(
() => OutlinedButton.icon(
@@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
).marginSymmetric(vertical: 2 * em),
Option(startmenu, label: 'Create start menu shortcuts')
.marginOnly(bottom: 7),
- Option(desktopicon, label: 'Create desktop icon'),
+ Option(desktopicon, label: 'Create desktop icon')
+ .marginOnly(bottom: 7),
+ Option(printer, label: 'Install {$appName} Printer'),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
String args = '';
if (startmenu.value) args += ' startmenu';
if (desktopicon.value) args += ' desktopicon';
+ if (printer.value) args += ' printer';
bind.installInstallMe(options: args, path: controller.text);
}
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index d6d243c5026..6671d041bbf 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -238,7 +238,7 @@ class _PortForwardPageState extends State
inputFormatters: inputFormatters,
decoration: InputDecoration(
hintText: hint,
- ))),
+ )).workaroundFreezeLinuxMint()),
);
}
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index f399f7cab68..9d366bcb0cf 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -118,11 +118,13 @@ class _PortForwardTabPageState extends State {
backgroundColor: Theme.of(context).colorScheme.background,
body: child),
)
- : Container(
- decoration: BoxDecoration(
- border: Border.all(color: MyTheme.color(context).border!)),
- child: child,
- );
+ : workaroundWindowBorder(
+ context,
+ Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.color(context).border!)),
+ child: child,
+ ));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index efd437e1ff7..ba698bd56b7 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -146,16 +146,8 @@ class _ConnectionTabPageState extends State {
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
- String msgConn;
- if (secure && direct) {
- msgConn = translate("Direct and encrypted connection");
- } else if (secure && !direct) {
- msgConn = translate("Relayed and encrypted connection");
- } else if (!secure && direct) {
- msgConn = translate("Direct and unencrypted connection");
- } else {
- msgConn = translate("Relayed and unencrypted connection");
- }
+ String msgConn = getConnectionText(
+ secure, direct, connectionType.stream_type.value);
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
@@ -212,14 +204,16 @@ class _ConnectionTabPageState extends State {
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
- : Obx(() => Container(
- decoration: BoxDecoration(
- border: Border.all(
- color: MyTheme.color(context).border!,
- width: stateGlobal.windowBorderWidth.value),
- ),
- child: child,
- ));
+ : workaroundWindowBorder(
+ context,
+ Obx(() => Container(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: MyTheme.color(context).border!,
+ width: stateGlobal.windowBorderWidth.value),
+ ),
+ child: child,
+ )));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
@@ -267,8 +261,10 @@ class _ConnectionTabPageState extends State {
style: style,
),
proc: () async {
- await DesktopMultiWindow.invokeMethod(kMainWindowId,
- kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId');
+ await DesktopMultiWindow.invokeMethod(
+ kMainWindowId,
+ kWindowEventMoveTabToNewWindow,
+ '${windowId()},$key,$sessionId,RemoteDesktop');
cancelFunc();
},
padding: padding,
@@ -415,8 +411,8 @@ class _ConnectionTabPageState extends State {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
- await setNewConnectWindowFrame(
- windowId(), id!, prePeerCount, display, screenRect);
+ await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
+ WindowType.RemoteDesktop, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 95d9f2c7c7d..4ee29756fc2 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -88,12 +88,14 @@ class _DesktopServerPageState extends State
);
return isLinux
? buildVirtualWindowFrame(context, body)
- : Container(
- decoration: BoxDecoration(
- border:
- Border.all(color: MyTheme.color(context).border!)),
- child: body,
- );
+ : workaroundWindowBorder(
+ context,
+ Container(
+ decoration: BoxDecoration(
+ border:
+ Border.all(color: MyTheme.color(context).border!)),
+ child: body,
+ ));
},
),
);
@@ -351,7 +353,10 @@ Widget buildConnectionCard(Client client) {
key: ValueKey(client.id),
children: [
_CmHeader(client: client),
- client.type_() != ClientType.remote || client.disconnected
+ client.type_() == ClientType.file ||
+ client.type_() == ClientType.portForward ||
+ client.type_() == ClientType.terminal ||
+ client.disconnected
? Offstage()
: _PrivilegeBoard(client: client),
Expanded(
@@ -495,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader>
"(${client.peerId})",
style: TextStyle(color: Colors.white, fontSize: 14),
),
- ).marginOnly(bottom: 10.0),
+ ),
+ if (client.type_() == ClientType.terminal)
+ FittedBox(
+ child: Text(
+ translate("Terminal"),
+ style: TextStyle(color: Colors.white70, fontSize: 12),
+ ),
+ ),
+ if (client.type_() == ClientType.file)
+ FittedBox(
+ child: Text(
+ translate("File Transfer"),
+ style: TextStyle(color: Colors.white70, fontSize: 12),
+ ),
+ ),
+ if (client.type_() == ClientType.camera)
+ FittedBox(
+ child: Text(
+ translate("View Camera"),
+ style: TextStyle(color: Colors.white70, fontSize: 12),
+ ),
+ ),
+ if (client.portForward.isNotEmpty)
+ FittedBox(
+ child: Text(
+ "Port Forward: ${client.portForward}",
+ style: TextStyle(color: Colors.white70, fontSize: 12),
+ ),
+ ),
+ SizedBox(height: 10.0),
FittedBox(
child: Row(
children: [
@@ -524,7 +558,8 @@ class _CmHeaderState extends State<_CmHeader>
Offstage(
offstage: !client.authorized ||
(client.type_() != ClientType.remote &&
- client.type_() != ClientType.file),
+ client.type_() != ClientType.file &&
+ client.type_() != ClientType.camera),
child: IconButton(
onPressed: () => checkClickTime(client.id, () {
if (client.type_() == ClientType.file) {
@@ -625,96 +660,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
padding: EdgeInsets.symmetric(horizontal: spacing),
mainAxisSpacing: spacing,
crossAxisSpacing: spacing,
- children: [
- buildPermissionIcon(
- client.keyboard,
- Icons.keyboard,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "keyboard", enabled: enabled);
- setState(() {
- client.keyboard = enabled;
- });
- },
- translate('Enable keyboard/mouse'),
- ),
- buildPermissionIcon(
- client.clipboard,
- Icons.assignment_rounded,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "clipboard", enabled: enabled);
- setState(() {
- client.clipboard = enabled;
- });
- },
- translate('Enable clipboard'),
- ),
- buildPermissionIcon(
- client.audio,
- Icons.volume_up_rounded,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "audio", enabled: enabled);
- setState(() {
- client.audio = enabled;
- });
- },
- translate('Enable audio'),
- ),
- buildPermissionIcon(
- client.file,
- Icons.upload_file_rounded,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "file", enabled: enabled);
- setState(() {
- client.file = enabled;
- });
- },
- translate('Enable file copy and paste'),
- ),
- buildPermissionIcon(
- client.restart,
- Icons.restart_alt_rounded,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "restart", enabled: enabled);
- setState(() {
- client.restart = enabled;
- });
- },
- translate('Enable remote restart'),
- ),
- buildPermissionIcon(
- client.recording,
- Icons.videocam_rounded,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id, name: "recording", enabled: enabled);
- setState(() {
- client.recording = enabled;
- });
- },
- translate('Enable recording session'),
- ),
- // only windows support block input
- if (isWindows)
- buildPermissionIcon(
- client.blockInput,
- Icons.block,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id,
- name: "block_input",
- enabled: enabled);
- setState(() {
- client.blockInput = enabled;
- });
- },
- translate('Enable blocking user input'),
- )
- ],
+ children: client.type_() == ClientType.camera
+ ? [
+ buildPermissionIcon(
+ client.audio,
+ Icons.volume_up_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "audio",
+ enabled: enabled);
+ setState(() {
+ client.audio = enabled;
+ });
+ },
+ translate('Enable audio'),
+ ),
+ buildPermissionIcon(
+ client.recording,
+ Icons.videocam_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "recording",
+ enabled: enabled);
+ setState(() {
+ client.recording = enabled;
+ });
+ },
+ translate('Enable recording session'),
+ ),
+ ]
+ : [
+ buildPermissionIcon(
+ client.keyboard,
+ Icons.keyboard,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "keyboard",
+ enabled: enabled);
+ setState(() {
+ client.keyboard = enabled;
+ });
+ },
+ translate('Enable keyboard/mouse'),
+ ),
+ buildPermissionIcon(
+ client.clipboard,
+ Icons.assignment_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "clipboard",
+ enabled: enabled);
+ setState(() {
+ client.clipboard = enabled;
+ });
+ },
+ translate('Enable clipboard'),
+ ),
+ buildPermissionIcon(
+ client.audio,
+ Icons.volume_up_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "audio",
+ enabled: enabled);
+ setState(() {
+ client.audio = enabled;
+ });
+ },
+ translate('Enable audio'),
+ ),
+ buildPermissionIcon(
+ client.file,
+ Icons.upload_file_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "file",
+ enabled: enabled);
+ setState(() {
+ client.file = enabled;
+ });
+ },
+ translate('Enable file copy and paste'),
+ ),
+ buildPermissionIcon(
+ client.restart,
+ Icons.restart_alt_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "restart",
+ enabled: enabled);
+ setState(() {
+ client.restart = enabled;
+ });
+ },
+ translate('Enable remote restart'),
+ ),
+ buildPermissionIcon(
+ client.recording,
+ Icons.videocam_rounded,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "recording",
+ enabled: enabled);
+ setState(() {
+ client.recording = enabled;
+ });
+ },
+ translate('Enable recording session'),
+ ),
+ // only windows support block input
+ if (isWindows)
+ buildPermissionIcon(
+ client.blockInput,
+ Icons.block,
+ (enabled) {
+ bind.cmSwitchPermission(
+ connId: client.id,
+ name: "block_input",
+ enabled: enabled);
+ setState(() {
+ client.blockInput = enabled;
+ });
+ },
+ translate('Enable blocking user input'),
+ )
+ ],
),
),
],
diff --git a/flutter/lib/desktop/pages/terminal_connection_manager.dart b/flutter/lib/desktop/pages/terminal_connection_manager.dart
new file mode 100644
index 00000000000..91b8baa9751
--- /dev/null
+++ b/flutter/lib/desktop/pages/terminal_connection_manager.dart
@@ -0,0 +1,98 @@
+import 'package:flutter/foundation.dart';
+import 'package:get/get.dart';
+import '../../models/model.dart';
+
+/// Manages terminal connections to ensure one FFI instance per peer
+class TerminalConnectionManager {
+ static final Map _connections = {};
+ static final Map _connectionRefCount = {};
+
+ // Track service IDs per peer
+ static final Map _serviceIds = {};
+
+ /// Get or create an FFI instance for a peer
+ static FFI getConnection({
+ required String peerId,
+ required String? password,
+ required bool? isSharedPassword,
+ required bool? forceRelay,
+ required String? connToken,
+ }) {
+ final existingFfi = _connections[peerId];
+ if (existingFfi != null && !existingFfi.closed) {
+ // Increment reference count
+ _connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1;
+ debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}');
+ return existingFfi;
+ }
+
+ // Create new FFI instance for first terminal
+ debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId');
+ final ffi = FFI(null);
+ ffi.start(
+ peerId,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ forceRelay: forceRelay,
+ connToken: connToken,
+ isTerminal: true,
+ );
+
+ _connections[peerId] = ffi;
+ _connectionRefCount[peerId] = 1;
+
+ // Register the FFI instance with Get for dependency injection
+ Get.put(ffi, tag: 'terminal_$peerId');
+
+ debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}');
+ return ffi;
+ }
+
+ /// Release a connection reference
+ static void releaseConnection(String peerId) {
+ final refCount = _connectionRefCount[peerId] ?? 0;
+ debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount');
+
+ if (refCount <= 1) {
+ // Last reference, close the connection
+ final ffi = _connections[peerId];
+ if (ffi != null) {
+ debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)');
+ ffi.close();
+ _connections.remove(peerId);
+ _connectionRefCount.remove(peerId);
+ Get.delete(tag: 'terminal_$peerId');
+ }
+ } else {
+ // Decrement reference count
+ _connectionRefCount[peerId] = refCount - 1;
+ debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}');
+ }
+ }
+
+ /// Check if a connection exists for a peer
+ static bool hasConnection(String peerId) {
+ final ffi = _connections[peerId];
+ return ffi != null && !ffi.closed;
+ }
+
+ /// Get existing connection without creating new one
+ static FFI? getExistingConnection(String peerId) {
+ return _connections[peerId];
+ }
+
+ /// Get connection count for debugging
+ static int getConnectionCount() => _connections.length;
+
+ /// Get terminal count for a peer
+ static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0;
+
+ /// Get service ID for a peer
+ static String? getServiceId(String peerId) => _serviceIds[peerId];
+
+ /// Set service ID for a peer
+ static void setServiceId(String peerId, String serviceId) {
+ _serviceIds[peerId] = serviceId;
+ debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId');
+ }
+}
\ No newline at end of file
diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart
new file mode 100644
index 00000000000..f28545415ee
--- /dev/null
+++ b/flutter/lib/desktop/pages/terminal_page.dart
@@ -0,0 +1,121 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:flutter_hbb/models/terminal_model.dart';
+import 'package:xterm/xterm.dart';
+import 'terminal_connection_manager.dart';
+
+class TerminalPage extends StatefulWidget {
+ const TerminalPage({
+ Key? key,
+ required this.id,
+ required this.password,
+ required this.tabController,
+ required this.isSharedPassword,
+ required this.terminalId,
+ this.forceRelay,
+ this.connToken,
+ }) : super(key: key);
+ final String id;
+ final String? password;
+ final DesktopTabController tabController;
+ final bool? forceRelay;
+ final bool? isSharedPassword;
+ final String? connToken;
+ final int terminalId;
+
+ @override
+ State createState() => _TerminalPageState();
+}
+
+class _TerminalPageState extends State
+ with AutomaticKeepAliveClientMixin {
+ late FFI _ffi;
+ late TerminalModel _terminalModel;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Use shared FFI instance from connection manager
+ _ffi = TerminalConnectionManager.getConnection(
+ peerId: widget.id,
+ password: widget.password,
+ isSharedPassword: widget.isSharedPassword,
+ forceRelay: widget.forceRelay,
+ connToken: widget.connToken,
+ );
+
+ // Create terminal model with specific terminal ID
+ _terminalModel = TerminalModel(_ffi, widget.terminalId);
+ debugPrint(
+ '[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
+
+ // Register this terminal model with FFI for event routing
+ _ffi.registerTerminalModel(widget.terminalId, _terminalModel);
+
+ // Initialize terminal connection
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ widget.tabController.onSelected?.call(widget.id);
+
+ // Check if this is a new connection or additional terminal
+ // Note: When a connection exists, the ref count will be > 1 after this terminal is added
+ final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
+ TerminalConnectionManager.getTerminalCount(widget.id) > 1;
+
+ if (!isExistingConnection) {
+ // First terminal - show loading dialog, wait for onReady
+ _ffi.dialogManager
+ .showLoading(translate('Connecting...'), onCancel: closeConnection);
+ } else {
+ // Additional terminal - connection already established
+ // Open the terminal directly
+ _terminalModel.openTerminal();
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ // Unregister terminal model from FFI
+ _ffi.unregisterTerminalModel(widget.terminalId);
+ _terminalModel.dispose();
+ // Release connection reference instead of closing directly
+ TerminalConnectionManager.releaseConnection(widget.id);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Scaffold(
+ backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+ body: TerminalView(
+ _terminalModel.terminal,
+ controller: _terminalModel.terminalController,
+ autofocus: true,
+ backgroundOpacity: 0.7,
+ padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
+ onSecondaryTapDown: (details, offset) async {
+ final selection = _terminalModel.terminalController.selection;
+ if (selection != null) {
+ final text = _terminalModel.terminal.buffer.getText(selection);
+ _terminalModel.terminalController.clearSelection();
+ await Clipboard.setData(ClipboardData(text: text));
+ } else {
+ final data = await Clipboard.getData('text/plain');
+ final text = data?.text;
+ if (text != null) {
+ _terminalModel.terminal.paste(text);
+ }
+ }
+ },
+ ),
+ );
+ }
+
+ @override
+ bool get wantKeepAlive => true;
+}
diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart
new file mode 100644
index 00000000000..754b309aec1
--- /dev/null
+++ b/flutter/lib/desktop/pages/terminal_tab_page.dart
@@ -0,0 +1,427 @@
+import 'dart:convert';
+
+import 'package:desktop_multi_window/desktop_multi_window.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/state_model.dart';
+import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
+import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:get/get.dart';
+
+import '../../models/platform_model.dart';
+import 'terminal_page.dart';
+import 'terminal_connection_manager.dart';
+import '../widgets/material_mod_popup_menu.dart' as mod_menu;
+import '../widgets/popup_menu.dart';
+import 'package:bot_toast/bot_toast.dart';
+
+class TerminalTabPage extends StatefulWidget {
+ final Map params;
+
+ const TerminalTabPage({Key? key, required this.params}) : super(key: key);
+
+ @override
+ State createState() => _TerminalTabPageState(params);
+}
+
+class _TerminalTabPageState extends State {
+ DesktopTabController get tabController => Get.find();
+
+ static const IconData selectedIcon = Icons.terminal;
+ static const IconData unselectedIcon = Icons.terminal_outlined;
+ int _nextTerminalId = 1;
+
+ _TerminalTabPageState(Map params) {
+ Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
+ tabController.onSelected = (id) {
+ WindowController.fromWindowId(windowId())
+ .setTitle(getWindowNameWithId(id));
+ };
+ tabController.onRemoved = (_, id) => onRemoveId(id);
+ final terminalId = params['terminalId'] ?? _nextTerminalId++;
+ tabController.add(_createTerminalTab(
+ peerId: params['id'],
+ terminalId: terminalId,
+ password: params['password'],
+ isSharedPassword: params['isSharedPassword'],
+ forceRelay: params['forceRelay'],
+ connToken: params['connToken'],
+ ));
+ }
+
+ TabInfo _createTerminalTab({
+ required String peerId,
+ required int terminalId,
+ String? password,
+ bool? isSharedPassword,
+ bool? forceRelay,
+ String? connToken,
+ }) {
+ final tabKey = '${peerId}_$terminalId';
+ return TabInfo(
+ key: tabKey,
+ label: '$peerId #$terminalId',
+ selectedIcon: selectedIcon,
+ unselectedIcon: unselectedIcon,
+ onTabCloseButton: () async {
+ // Close the terminal session first
+ final ffi = TerminalConnectionManager.getExistingConnection(peerId);
+ if (ffi != null) {
+ final terminalModel = ffi.terminalModels[terminalId];
+ if (terminalModel != null) {
+ await terminalModel.closeTerminal();
+ }
+ }
+ // Then close the tab
+ tabController.closeBy(tabKey);
+ },
+ page: TerminalPage(
+ key: ValueKey(tabKey),
+ id: peerId,
+ terminalId: terminalId,
+ password: password,
+ isSharedPassword: isSharedPassword,
+ tabController: tabController,
+ forceRelay: forceRelay,
+ connToken: connToken,
+ ),
+ );
+ }
+
+ Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
+ final List> menu = [];
+ const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
+
+ // New tab menu item
+ menu.add(MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('New tab'),
+ style: style,
+ ),
+ proc: () {
+ _addNewTerminal(peerId);
+ cancelFunc();
+ // Also try to close any BotToast overlays
+ BotToast.cleanAll();
+ },
+ padding: padding,
+ ));
+
+ menu.add(MenuEntryDivider());
+
+ menu.add(MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: translate('Keep terminal sessions on disconnect'),
+ getter: () async {
+ final ffi = Get.find(tag: 'terminal_$peerId');
+ return bind.sessionGetToggleOptionSync(
+ sessionId: ffi.sessionId,
+ arg: kOptionTerminalPersistent,
+ );
+ },
+ setter: (bool v) async {
+ final ffi = Get.find(tag: 'terminal_$peerId');
+ await bind.sessionToggleOption(
+ sessionId: ffi.sessionId,
+ value: kOptionTerminalPersistent,
+ );
+ },
+ padding: padding,
+ ));
+
+ return mod_menu.PopupMenu(
+ items: menu
+ .map((e) => e.build(
+ context,
+ const MenuConfig(
+ commonColor: CustomPopupMenuTheme.commonColor,
+ height: CustomPopupMenuTheme.height,
+ dividerHeight: CustomPopupMenuTheme.dividerHeight,
+ ),
+ ))
+ .expand((i) => i)
+ .toList(),
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Add keyboard shortcut handler
+ HardwareKeyboard.instance.addHandler(_handleKeyEvent);
+
+ rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
+ print(
+ "[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId");
+ if (call.method == kWindowEventNewTerminal) {
+ final args = jsonDecode(call.arguments);
+ final id = args['id'];
+ windowOnTop(windowId());
+ // Allow multiple terminals for the same connection
+ final terminalId = args['terminalId'] ?? _nextTerminalId++;
+ tabController.add(_createTerminalTab(
+ peerId: id,
+ terminalId: terminalId,
+ password: args['password'],
+ isSharedPassword: args['isSharedPassword'],
+ forceRelay: args['forceRelay'],
+ connToken: args['connToken'],
+ ));
+ } else if (call.method == kWindowEventRestoreTerminalSessions) {
+ _restoreSessions(call.arguments);
+ } else if (call.method == "onDestroy") {
+ tabController.clear();
+ } else if (call.method == kWindowActionRebuild) {
+ reloadCurrentWindow();
+ } else if (call.method == kWindowEventActiveSession) {
+ if (tabController.state.value.tabs.isEmpty) {
+ return false;
+ }
+ final currentTab = tabController.state.value.selectedTabInfo;
+ assert(call.arguments is String,
+ "Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
+ if (currentTab.key.startsWith(call.arguments)) {
+ windowOnTop(windowId());
+ return true;
+ }
+ return false;
+ }
+ });
+ Future.delayed(Duration.zero, () {
+ restoreWindowPosition(WindowType.Terminal, windowId: windowId());
+ });
+ }
+
+ @override
+ void dispose() {
+ HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
+ super.dispose();
+ }
+
+ Future _restoreSessions(String arguments) async {
+ Map? args;
+ try {
+ args = jsonDecode(arguments) as Map;
+ } catch (e) {
+ debugPrint("Error parsing JSON arguments in _restoreSessions: $e");
+ return;
+ }
+ final persistentSessions =
+ args['persistent_sessions'] as List? ?? [];
+ final sortedSessions = persistentSessions.whereType().toList()..sort();
+ for (final terminalId in sortedSessions) {
+ _addNewTerminalForCurrentPeer(terminalId: terminalId);
+ // A delay is required to ensure the UI has sufficient time to update
+ // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
+ // may be called prematurely while the tab widget is still in the tab controller.
+ // This behavior is likely due to a race condition between the UI rendering lifecycle
+ // and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()`
+ // to wait for the previous page to be ready were unsuccessful, as the observed call sequence is:
+ // `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`.
+ // The `Future.delayed` approach mitigates this issue by introducing a buffer period,
+ // allowing the UI to stabilize before proceeding.
+ await Future.delayed(const Duration(milliseconds: 300));
+ }
+ }
+
+ bool _handleKeyEvent(KeyEvent event) {
+ if (event is KeyDownEvent) {
+ // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms
+ if (event.logicalKey == LogicalKeyboardKey.keyT) {
+ if (isMacOS &&
+ HardwareKeyboard.instance.isMetaPressed &&
+ !HardwareKeyboard.instance.isShiftPressed) {
+ // macOS: Cmd+T (standard for new tab)
+ _addNewTerminalForCurrentPeer();
+ return true;
+ } else if (!isMacOS &&
+ HardwareKeyboard.instance.isControlPressed &&
+ HardwareKeyboard.instance.isShiftPressed) {
+ // Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal)
+ _addNewTerminalForCurrentPeer();
+ return true;
+ }
+ }
+
+ // Use Cmd+W on macOS, Ctrl+Shift+W on other platforms
+ if (event.logicalKey == LogicalKeyboardKey.keyW) {
+ if (isMacOS &&
+ HardwareKeyboard.instance.isMetaPressed &&
+ !HardwareKeyboard.instance.isShiftPressed) {
+ // macOS: Cmd+W (standard for close tab)
+ final currentTab = tabController.state.value.selectedTabInfo;
+ if (tabController.state.value.tabs.length > 1) {
+ tabController.closeBy(currentTab.key);
+ return true;
+ }
+ } else if (!isMacOS &&
+ HardwareKeyboard.instance.isControlPressed &&
+ HardwareKeyboard.instance.isShiftPressed) {
+ // Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
+ final currentTab = tabController.state.value.selectedTabInfo;
+ if (tabController.state.value.tabs.length > 1) {
+ tabController.closeBy(currentTab.key);
+ return true;
+ }
+ }
+ }
+
+ // Use Alt+Left/Right for tab navigation (avoids conflicts)
+ if (HardwareKeyboard.instance.isAltPressed) {
+ if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+ // Previous tab
+ final currentIndex = tabController.state.value.selected;
+ if (currentIndex > 0) {
+ tabController.jumpTo(currentIndex - 1);
+ }
+ return true;
+ } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+ // Next tab
+ final currentIndex = tabController.state.value.selected;
+ if (currentIndex < tabController.length - 1) {
+ tabController.jumpTo(currentIndex + 1);
+ }
+ return true;
+ }
+ }
+
+ // Check for Cmd/Ctrl + Number (switch to specific tab)
+ final numberKeys = [
+ LogicalKeyboardKey.digit1,
+ LogicalKeyboardKey.digit2,
+ LogicalKeyboardKey.digit3,
+ LogicalKeyboardKey.digit4,
+ LogicalKeyboardKey.digit5,
+ LogicalKeyboardKey.digit6,
+ LogicalKeyboardKey.digit7,
+ LogicalKeyboardKey.digit8,
+ LogicalKeyboardKey.digit9,
+ ];
+
+ for (int i = 0; i < numberKeys.length; i++) {
+ if (event.logicalKey == numberKeys[i] &&
+ ((isMacOS && HardwareKeyboard.instance.isMetaPressed) ||
+ (!isMacOS && HardwareKeyboard.instance.isControlPressed))) {
+ if (i < tabController.length) {
+ tabController.jumpTo(i);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ void _addNewTerminal(String peerId, {int? terminalId}) {
+ // Find first tab for this peer to get connection parameters
+ final firstTab = tabController.state.value.tabs.firstWhere(
+ (tab) => tab.key.startsWith('$peerId\_'),
+ );
+ if (firstTab.page is TerminalPage) {
+ final page = firstTab.page as TerminalPage;
+ final newTerminalId = terminalId ?? _nextTerminalId++;
+ if (terminalId != null && terminalId >= _nextTerminalId) {
+ _nextTerminalId = terminalId + 1;
+ }
+ tabController.add(_createTerminalTab(
+ peerId: peerId,
+ terminalId: newTerminalId,
+ password: page.password,
+ isSharedPassword: page.isSharedPassword,
+ forceRelay: page.forceRelay,
+ connToken: page.connToken,
+ ));
+ }
+ }
+
+ void _addNewTerminalForCurrentPeer({int? terminalId}) {
+ final currentTab = tabController.state.value.selectedTabInfo;
+ final parts = currentTab.key.split('_');
+ if (parts.isNotEmpty) {
+ final peerId = parts[0];
+ _addNewTerminal(peerId, terminalId: terminalId);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final child = Scaffold(
+ backgroundColor: Theme.of(context).cardColor,
+ body: DesktopTab(
+ controller: tabController,
+ onWindowCloseButton: handleWindowCloseButton,
+ tail: _buildAddButton(),
+ selectedBorderColor: MyTheme.accent,
+ labelGetter: DesktopTab.tablabelGetter,
+ tabMenuBuilder: (key) {
+ // Extract peerId from tab key (format: "peerId_terminalId")
+ final parts = key.split('_');
+ if (parts.isEmpty) return Container();
+ final peerId = parts[0];
+ return _tabMenuBuilder(peerId, () {});
+ },
+ ));
+ final tabWidget = isLinux
+ ? buildVirtualWindowFrame(context, child)
+ : workaroundWindowBorder(
+ context,
+ Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.color(context).border!)),
+ child: child,
+ ));
+ return isMacOS || kUseCompatibleUiMode
+ ? tabWidget
+ : SubWindowDragToResizeArea(
+ child: tabWidget,
+ resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: subWindowManagerEnableResizeEdges,
+ windowId: stateGlobal.windowId,
+ );
+ }
+
+ void onRemoveId(String id) {
+ if (tabController.state.value.tabs.isEmpty) {
+ WindowController.fromWindowId(windowId()).close();
+ }
+ }
+
+ int windowId() {
+ return widget.params["windowId"];
+ }
+
+ Widget _buildAddButton() {
+ return ActionIcon(
+ message: 'New tab',
+ icon: IconFont.add,
+ onTap: () {
+ _addNewTerminalForCurrentPeer();
+ },
+ isClose: false,
+ );
+ }
+
+ Future handleWindowCloseButton() async {
+ final connLength = tabController.state.value.tabs.length;
+ if (connLength <= 1) {
+ tabController.clear();
+ return true;
+ } else {
+ final bool res;
+ if (!option2bool(kOptionEnableConfirmClosingTabs,
+ bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
+ res = true;
+ } else {
+ res = await closeConfirmDialog();
+ }
+ if (res) {
+ tabController.clear();
+ }
+ return res;
+ }
+ }
+}
diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart
new file mode 100644
index 00000000000..a1cc5c8a074
--- /dev/null
+++ b/flutter/lib/desktop/pages/view_camera_page.dart
@@ -0,0 +1,728 @@
+import 'dart:async';
+
+import 'package:desktop_multi_window/desktop_multi_window.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common/widgets/remote_input.dart';
+import 'package:get/get.dart';
+import 'package:provider/provider.dart';
+import 'package:wakelock_plus/wakelock_plus.dart';
+import 'package:flutter_hbb/models/state_model.dart';
+
+import '../../consts.dart';
+import '../../common/widgets/overlay.dart';
+import '../../common.dart';
+import '../../common/widgets/dialog.dart';
+import '../../common/widgets/toolbar.dart';
+import '../../models/model.dart';
+import '../../models/platform_model.dart';
+import '../../common/shared_state.dart';
+import '../../utils/image.dart';
+import '../widgets/remote_toolbar.dart';
+import '../widgets/kb_layout_type_chooser.dart';
+import '../widgets/tabbar_widget.dart';
+
+import 'package:flutter_hbb/native/custom_cursor.dart'
+ if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
+
+final SimpleWrapper _firstEnterImage = SimpleWrapper(false);
+
+// Used to skip session close if "move to new window" is clicked.
+final Map closeSessionOnDispose = {};
+
+class ViewCameraPage extends StatefulWidget {
+ ViewCameraPage({
+ Key? key,
+ required this.id,
+ required this.toolbarState,
+ this.sessionId,
+ this.tabWindowId,
+ this.password,
+ this.display,
+ this.displays,
+ this.tabController,
+ this.connToken,
+ this.forceRelay,
+ this.isSharedPassword,
+ }) : super(key: key) {
+ initSharedStates(id);
+ }
+
+ final String id;
+ final SessionID? sessionId;
+ final int? tabWindowId;
+ final int? display;
+ final List? displays;
+ final String? password;
+ final ToolbarState toolbarState;
+ final bool? forceRelay;
+ final bool? isSharedPassword;
+ final String? connToken;
+ final SimpleWrapper?> _lastState = SimpleWrapper(null);
+ final DesktopTabController? tabController;
+
+ FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
+
+ @override
+ State createState() {
+ final state = _ViewCameraPageState(id);
+ _lastState.value = state;
+ return state;
+ }
+}
+
+class _ViewCameraPageState extends State
+ with AutomaticKeepAliveClientMixin, MultiWindowListener {
+ Timer? _timer;
+ String keyboardMode = "legacy";
+ bool _isWindowBlur = false;
+ final _cursorOverImage = false.obs;
+
+ var _blockableOverlayState = BlockableOverlayState();
+
+ final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
+
+ // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
+ // to identify the toolbar instance and its callback function.
+ int? _instanceIdOnEnterOrLeaveImage4Toolbar;
+ Function(bool)? _onEnterOrLeaveImage4Toolbar;
+
+ late FFI _ffi;
+
+ SessionID get sessionId => _ffi.sessionId;
+
+ _ViewCameraPageState(String id) {
+ _initStates(id);
+ }
+
+ void _initStates(String id) {}
+
+ @override
+ void initState() {
+ super.initState();
+ _ffi = FFI(widget.sessionId);
+ Get.put(_ffi, tag: widget.id);
+ _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
+ showKBLayoutTypeChooserIfNeeded(
+ _ffi.ffiModel.pi.platform, _ffi.dialogManager);
+ _ffi.recordingModel
+ .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
+ });
+ _ffi.start(
+ widget.id,
+ isViewCamera: true,
+ password: widget.password,
+ isSharedPassword: widget.isSharedPassword,
+ forceRelay: widget.forceRelay,
+ tabWindowId: widget.tabWindowId,
+ display: widget.display,
+ displays: widget.displays,
+ connToken: widget.connToken,
+ );
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
+ _ffi.dialogManager
+ .showLoading(translate('Connecting...'), onCancel: closeConnection);
+ });
+ if (!isLinux) {
+ WakelockPlus.enable();
+ }
+
+ _ffi.ffiModel.updateEventListener(sessionId, widget.id);
+ if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
+ _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
+ _ffi.dialogManager.loadMobileActionsOverlayVisible();
+ DesktopMultiWindow.addListener(this);
+ // if (!_isCustomCursorInited) {
+ // customCursorController.registerNeedUpdateCursorCallback(
+ // (String? lastKey, String? currentKey) async {
+ // if (_firstEnterImage.value) {
+ // _firstEnterImage.value = false;
+ // return true;
+ // }
+ // return lastKey == null || lastKey != currentKey;
+ // });
+ // _isCustomCursorInited = true;
+ // }
+
+ _blockableOverlayState.applyFfi(_ffi);
+ // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ widget.tabController?.onSelected?.call(widget.id);
+ });
+ }
+
+ @override
+ void onWindowBlur() {
+ super.onWindowBlur();
+ // On windows, we use `focus` way to handle keyboard better.
+ // Now on Linux, there's some rdev issues which will break the input.
+ // We disable the `focus` way for non-Windows temporarily.
+ if (isWindows) {
+ _isWindowBlur = true;
+ // unfocus the primary-focus when the whole window is lost focus,
+ // and let OS to handle events instead.
+ _rawKeyFocusNode.unfocus();
+ }
+ stateGlobal.isFocused.value = false;
+ }
+
+ @override
+ void onWindowFocus() {
+ super.onWindowFocus();
+ // See [onWindowBlur].
+ if (isWindows) {
+ _isWindowBlur = false;
+ }
+ stateGlobal.isFocused.value = true;
+ }
+
+ @override
+ void onWindowRestore() {
+ super.onWindowRestore();
+ // On windows, we use `onWindowRestore` way to handle window restore from
+ // a minimized state.
+ if (isWindows) {
+ _isWindowBlur = false;
+ }
+ if (!isLinux) {
+ WakelockPlus.enable();
+ }
+ }
+
+ // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
+ @override
+ void onWindowMaximize() {
+ super.onWindowMaximize();
+ if (!isLinux) {
+ WakelockPlus.enable();
+ }
+ }
+
+ @override
+ void onWindowMinimize() {
+ super.onWindowMinimize();
+ if (!isLinux) {
+ WakelockPlus.disable();
+ }
+ }
+
+ @override
+ void onWindowEnterFullScreen() {
+ super.onWindowEnterFullScreen();
+ if (isMacOS) {
+ stateGlobal.setFullscreen(true);
+ }
+ }
+
+ @override
+ void onWindowLeaveFullScreen() {
+ super.onWindowLeaveFullScreen();
+ if (isMacOS) {
+ stateGlobal.setFullscreen(false);
+ }
+ }
+
+ @override
+ Future dispose() async {
+ final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
+
+ // https://github.com/flutter/flutter/issues/64935
+ super.dispose();
+ debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
+ _ffi.textureModel.onViewCameraPageDispose(closeSession);
+ if (closeSession) {
+ // ensure we leave this session, this is a double check
+ _ffi.inputModel.enterOrLeave(false);
+ }
+ DesktopMultiWindow.removeListener(this);
+ _ffi.dialogManager.hideMobileActionsOverlay();
+ _ffi.imageModel.disposeImage();
+ _ffi.cursorModel.disposeImages();
+ _rawKeyFocusNode.dispose();
+ await _ffi.close(closeSession: closeSession);
+ _timer?.cancel();
+ _ffi.dialogManager.dismissAll();
+ if (closeSession) {
+ await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
+ overlays: SystemUiOverlay.values);
+ }
+ if (!isLinux) {
+ await WakelockPlus.disable();
+ }
+ await Get.delete(tag: widget.id);
+ removeSharedStates(widget.id);
+ }
+
+ Widget emptyOverlay() => BlockableOverlay(
+ /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+ /// see override build() in [BlockableOverlay]
+ state: _blockableOverlayState,
+ underlying: Container(
+ color: Colors.transparent,
+ ),
+ );
+
+ Widget buildBody(BuildContext context) {
+ remoteToolbar(BuildContext context) => RemoteToolbar(
+ id: widget.id,
+ ffi: _ffi,
+ state: widget.toolbarState,
+ onEnterOrLeaveImageSetter: (id, func) {
+ _instanceIdOnEnterOrLeaveImage4Toolbar = id;
+ _onEnterOrLeaveImage4Toolbar = func;
+ },
+ onEnterOrLeaveImageCleaner: (id) {
+ // If _instanceIdOnEnterOrLeaveImage4Toolbar != id
+ // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
+ if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
+ _instanceIdOnEnterOrLeaveImage4Toolbar = null;
+ _onEnterOrLeaveImage4Toolbar = null;
+ }
+ },
+ setRemoteState: setState,
+ );
+
+ bodyWidget() {
+ return Stack(
+ children: [
+ Container(
+ color: kColorCanvas,
+ child: getBodyForDesktop(context),
+ ),
+ Stack(
+ children: [
+ _ffi.ffiModel.pi.isSet.isTrue &&
+ _ffi.ffiModel.waitForFirstImage.isTrue
+ ? emptyOverlay()
+ : () {
+ if (!_ffi.ffiModel.isPeerAndroid) {
+ return Offstage();
+ } else {
+ return Obx(() => Offstage(
+ offstage: _ffi.dialogManager
+ .mobileActionsOverlayVisible.isFalse,
+ child: Overlay(initialEntries: [
+ makeMobileActionsOverlayEntry(
+ () => _ffi.dialogManager
+ .setMobileActionsOverlayVisible(false),
+ ffi: _ffi,
+ )
+ ]),
+ ));
+ }
+ }(),
+ // Use Overlay to enable rebuild every time on menu button click.
+ _ffi.ffiModel.pi.isSet.isTrue
+ ? Overlay(
+ initialEntries: [OverlayEntry(builder: remoteToolbar)])
+ : remoteToolbar(context),
+ _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
+ ],
+ ),
+ ],
+ );
+ }
+
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: Obx(() {
+ final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
+ _ffi.ffiModel.waitForFirstImage.isFalse;
+ if (imageReady) {
+ // If the privacy mode(disable physical displays) is switched,
+ // we should not dismiss the dialog immediately.
+ if (DateTime.now().difference(togglePrivacyModeTime) >
+ const Duration(milliseconds: 3000)) {
+ // `dismissAll()` is to ensure that the state is clean.
+ // It's ok to call dismissAll() here.
+ _ffi.dialogManager.dismissAll();
+ // Recreate the block state to refresh the state.
+ _blockableOverlayState = BlockableOverlayState();
+ _blockableOverlayState.applyFfi(_ffi);
+ }
+ // Block the whole `bodyWidget()` when dialog shows.
+ return BlockableOverlay(
+ underlying: bodyWidget(),
+ state: _blockableOverlayState,
+ );
+ } else {
+ // `_blockableOverlayState` is not recreated here.
+ // The toolbar's block state won't work properly when reconnecting, but that's okay.
+ return bodyWidget();
+ }
+ }),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return WillPopScope(
+ onWillPop: () async {
+ clientClose(sessionId, _ffi.dialogManager);
+ return false;
+ },
+ child: MultiProvider(providers: [
+ ChangeNotifierProvider.value(value: _ffi.ffiModel),
+ ChangeNotifierProvider.value(value: _ffi.imageModel),
+ ChangeNotifierProvider.value(value: _ffi.cursorModel),
+ ChangeNotifierProvider.value(value: _ffi.canvasModel),
+ ChangeNotifierProvider.value(value: _ffi.recordingModel),
+ ], child: buildBody(context)));
+ }
+
+ void enterView(PointerEnterEvent evt) {
+ _cursorOverImage.value = true;
+ _firstEnterImage.value = true;
+ if (_onEnterOrLeaveImage4Toolbar != null) {
+ try {
+ _onEnterOrLeaveImage4Toolbar!(true);
+ } catch (e) {
+ //
+ }
+ }
+ // See [onWindowBlur].
+ if (!isWindows) {
+ if (!_rawKeyFocusNode.hasFocus) {
+ _rawKeyFocusNode.requestFocus();
+ }
+ _ffi.inputModel.enterOrLeave(true);
+ }
+ }
+
+ void leaveView(PointerExitEvent evt) {
+ if (_ffi.ffiModel.keyboard) {
+ _ffi.inputModel.tryMoveEdgeOnExit(evt.position);
+ }
+
+ _cursorOverImage.value = false;
+ _firstEnterImage.value = false;
+ if (_onEnterOrLeaveImage4Toolbar != null) {
+ try {
+ _onEnterOrLeaveImage4Toolbar!(false);
+ } catch (e) {
+ //
+ }
+ }
+ // See [onWindowBlur].
+ if (!isWindows) {
+ _ffi.inputModel.enterOrLeave(false);
+ }
+ }
+
+ Widget _buildRawTouchAndPointerRegion(
+ Widget child,
+ PointerEnterEventListener? onEnter,
+ PointerExitEventListener? onExit,
+ ) {
+ return RawTouchGestureDetectorRegion(
+ child: _buildRawPointerMouseRegion(child, onEnter, onExit),
+ ffi: _ffi,
+ isCamera: true,
+ );
+ }
+
+ Widget _buildRawPointerMouseRegion(
+ Widget child,
+ PointerEnterEventListener? onEnter,
+ PointerExitEventListener? onExit,
+ ) {
+ return CameraRawPointerMouseRegion(
+ onEnter: onEnter,
+ onExit: onExit,
+ onPointerDown: (event) {
+ // A double check for blur status.
+ // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
+ // Sometimes the system does not send the necessary focus event to flutter. We should manually
+ // handle this inconsistent status by setting `_isWindowBlur` to false. So we can
+ // ensure the grab-key thread is running when our users are clicking the remote canvas.
+ if (_isWindowBlur) {
+ debugPrint(
+ "Unexpected status: onPointerDown is triggered while the remote window is in blur status");
+ _isWindowBlur = false;
+ }
+ if (!_rawKeyFocusNode.hasFocus) {
+ _rawKeyFocusNode.requestFocus();
+ }
+ },
+ inputModel: _ffi.inputModel,
+ child: child,
+ );
+ }
+
+ Widget getBodyForDesktop(BuildContext context) {
+ var paints = [
+ MouseRegion(onEnter: (evt) {
+ if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
+ }, onExit: (evt) {
+ if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
+ }, child: LayoutBuilder(builder: (context, constraints) {
+ final c = Provider.of(context, listen: false);
+ Future.delayed(Duration.zero, () => c.updateViewStyle());
+ final peerDisplay = CurrentDisplayState.find(widget.id);
+ return Obx(
+ () => _ffi.ffiModel.pi.isSet.isFalse
+ ? Container(color: Colors.transparent)
+ : Obx(() {
+ widget.toolbarState.initShow(sessionId);
+ _ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
+ return ImagePaint(
+ id: widget.id,
+ cursorOverImage: _cursorOverImage,
+ listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
+ child, enterView, leaveView),
+ ffi: _ffi,
+ );
+ }),
+ );
+ }))
+ ];
+
+ paints.add(
+ Positioned(
+ top: 10,
+ right: 10,
+ child: _buildRawTouchAndPointerRegion(
+ QualityMonitor(_ffi.qualityMonitorModel), null, null),
+ ),
+ );
+ return Stack(
+ children: paints,
+ );
+ }
+
+ @override
+ bool get wantKeepAlive => true;
+}
+
+class ImagePaint extends StatefulWidget {
+ final FFI ffi;
+ final String id;
+ final RxBool cursorOverImage;
+ final Widget Function(Widget)? listenerBuilder;
+
+ ImagePaint(
+ {Key? key,
+ required this.ffi,
+ required this.id,
+ required this.cursorOverImage,
+ this.listenerBuilder})
+ : super(key: key);
+
+ @override
+ State createState() => _ImagePaintState();
+}
+
+class _ImagePaintState extends State {
+ String get id => widget.id;
+ RxBool get cursorOverImage => widget.cursorOverImage;
+ Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
+
+ @override
+ Widget build(BuildContext context) {
+ final m = Provider.of(context);
+ var c = Provider.of(context);
+ final s = c.scale;
+
+ bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
+
+ if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
+ final paintWidth = c.getDisplayWidth() * s;
+ final paintHeight = c.getDisplayHeight() * s;
+ final paintSize = Size(paintWidth, paintHeight);
+ final paintWidget =
+ m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
+ ? _BuildPaintTextureRender(
+ c, s, Offset.zero, paintSize, isViewOriginal())
+ : _buildScrollbarNonTextureRender(m, paintSize, s);
+ return NotificationListener(
+ onNotification: (notification) {
+ c.updateScrollPercent();
+ return false;
+ },
+ child: Container(
+ child: _buildCrossScrollbarFromLayout(
+ context,
+ _buildListener(paintWidget),
+ c.size,
+ paintSize,
+ c.scrollHorizontal,
+ c.scrollVertical,
+ )),
+ );
+ } else {
+ if (c.size.width > 0 && c.size.height > 0) {
+ final paintWidget =
+ m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
+ ? _BuildPaintTextureRender(
+ c,
+ s,
+ Offset(
+ isLinux ? c.x.toInt().toDouble() : c.x,
+ isLinux ? c.y.toInt().toDouble() : c.y,
+ ),
+ c.size,
+ isViewOriginal())
+ : _buildScrollAutoNonTextureRender(m, c, s);
+ return Container(child: _buildListener(paintWidget));
+ } else {
+ return Container();
+ }
+ }
+ }
+
+ Widget _buildScrollbarNonTextureRender(
+ ImageModel m, Size imageSize, double s) {
+ return CustomPaint(
+ size: imageSize,
+ painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
+ );
+ }
+
+ Widget _buildScrollAutoNonTextureRender(
+ ImageModel m, CanvasModel c, double s) {
+ return CustomPaint(
+ size: Size(c.size.width, c.size.height),
+ painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
+ );
+ }
+
+ Widget _BuildPaintTextureRender(
+ CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
+ final ffiModel = c.parent.target!.ffiModel;
+ final displays = ffiModel.pi.getCurDisplays();
+ final children = [];
+ final rect = ffiModel.rect;
+ if (rect == null) {
+ return Container();
+ }
+ final curDisplay = ffiModel.pi.currentDisplay;
+ for (var i = 0; i < displays.length; i++) {
+ final textureId = widget.ffi.textureModel
+ .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
+ if (true) {
+ // both "textureId.value != -1" and "true" seems ok
+ children.add(Positioned(
+ left: (displays[i].x - rect.left) * s + offset.dx,
+ top: (displays[i].y - rect.top) * s + offset.dy,
+ width: displays[i].width * s,
+ height: displays[i].height * s,
+ child: Obx(() => Texture(
+ textureId: textureId.value,
+ filterQuality:
+ isViewOriginal ? FilterQuality.none : FilterQuality.low,
+ )),
+ ));
+ }
+ }
+ return SizedBox(
+ width: size.width,
+ height: size.height,
+ child: Stack(children: children),
+ );
+ }
+
+ MouseCursor _buildCustomCursor(BuildContext context, double scale) {
+ final cursor = Provider.of(context);
+ final cache = cursor.cache ?? preDefaultCursor.cache;
+ return buildCursorOfCache(cursor, scale, cache);
+ }
+
+ MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
+ final cursor = Provider.of(context);
+ final cache = preForbiddenCursor.cache;
+ return buildCursorOfCache(cursor, scale, cache);
+ }
+
+ Widget _buildCrossScrollbarFromLayout(
+ BuildContext context,
+ Widget child,
+ Size layoutSize,
+ Size size,
+ ScrollController horizontal,
+ ScrollController vertical,
+ ) {
+ var widget = child;
+ if (layoutSize.width < size.width) {
+ widget = ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
+ child: SingleChildScrollView(
+ controller: horizontal,
+ scrollDirection: Axis.horizontal,
+ physics: cursorOverImage.isTrue
+ ? const NeverScrollableScrollPhysics()
+ : null,
+ child: widget,
+ ),
+ );
+ } else {
+ widget = Row(
+ children: [
+ Container(
+ width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
+ ),
+ widget,
+ ],
+ );
+ }
+ if (layoutSize.height < size.height) {
+ widget = ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
+ child: SingleChildScrollView(
+ controller: vertical,
+ physics: cursorOverImage.isTrue
+ ? const NeverScrollableScrollPhysics()
+ : null,
+ child: widget,
+ ),
+ );
+ } else {
+ widget = Column(
+ children: [
+ Container(
+ height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
+ ),
+ widget,
+ ],
+ );
+ }
+ if (layoutSize.width < size.width) {
+ widget = RawScrollbar(
+ thickness: kScrollbarThickness,
+ thumbColor: Colors.grey,
+ controller: horizontal,
+ thumbVisibility: false,
+ trackVisibility: false,
+ notificationPredicate: layoutSize.height < size.height
+ ? (notification) => notification.depth == 1
+ : defaultScrollNotificationPredicate,
+ child: widget,
+ );
+ }
+ if (layoutSize.height < size.height) {
+ widget = RawScrollbar(
+ thickness: kScrollbarThickness,
+ thumbColor: Colors.grey,
+ controller: vertical,
+ thumbVisibility: false,
+ trackVisibility: false,
+ child: widget,
+ );
+ }
+
+ return Container(
+ child: widget,
+ width: layoutSize.width,
+ height: layoutSize.height,
+ );
+ }
+
+ Widget _buildListener(Widget child) {
+ if (listenerBuilder != null) {
+ return listenerBuilder!(child);
+ } else {
+ return child;
+ }
+ }
+}
diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart
new file mode 100644
index 00000000000..a31ba0fffc0
--- /dev/null
+++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart
@@ -0,0 +1,491 @@
+import 'dart:convert';
+import 'dart:async';
+import 'dart:ui' as ui;
+
+import 'package:desktop_multi_window/desktop_multi_window.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/common/shared_state.dart';
+import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/input_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
+import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
+import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
+import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
+import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
+ as mod_menu;
+import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
+import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:get/get.dart';
+import 'package:bot_toast/bot_toast.dart';
+
+import '../../models/platform_model.dart';
+
+class _MenuTheme {
+ static const Color blueColor = MyTheme.button;
+ // kMinInteractiveDimension
+ static const double height = 20.0;
+ static const double dividerHeight = 12.0;
+}
+
+class ViewCameraTabPage extends StatefulWidget {
+ final Map params;
+
+ const ViewCameraTabPage({Key? key, required this.params}) : super(key: key);
+
+ @override
+ State createState() => _ViewCameraTabPageState(params);
+}
+
+class _ViewCameraTabPageState extends State {
+ final tabController =
+ Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera));
+ final contentKey = UniqueKey();
+ static const IconData selectedIcon = Icons.desktop_windows_sharp;
+ static const IconData unselectedIcon = Icons.desktop_windows_outlined;
+
+ String? peerId;
+ bool _isScreenRectSet = false;
+ int? _display;
+
+ var connectionMap = RxList.empty(growable: true);
+
+ _ViewCameraTabPageState(Map params) {
+ RemoteCountState.init();
+ peerId = params['id'];
+ final sessionId = params['session_id'];
+ final tabWindowId = params['tab_window_id'];
+ final display = params['display'];
+ final displays = params['displays'];
+ final screenRect = parseParamScreenRect(params);
+ _isScreenRectSet = screenRect != null;
+ _display = display as int?;
+ tryMoveToScreenAndSetFullscreen(screenRect);
+ if (peerId != null) {
+ ConnectionTypeState.init(peerId!);
+ tabController.onSelected = (id) {
+ final viewCameraPage = tabController.widget(id);
+ if (viewCameraPage is ViewCameraPage) {
+ final ffi = viewCameraPage.ffi;
+ bind.setCurSessionId(sessionId: ffi.sessionId);
+ }
+ WindowController.fromWindowId(params['windowId'])
+ .setTitle(getWindowNameWithId(id));
+ UnreadChatCountState.find(id).value = 0;
+ };
+ tabController.add(TabInfo(
+ key: peerId!,
+ label: peerId!,
+ selectedIcon: selectedIcon,
+ unselectedIcon: unselectedIcon,
+ onTabCloseButton: () => tabController.closeBy(peerId),
+ page: ViewCameraPage(
+ key: ValueKey(peerId),
+ id: peerId!,
+ sessionId: sessionId == null ? null : SessionID(sessionId),
+ tabWindowId: tabWindowId,
+ display: display,
+ displays: displays?.cast(),
+ password: params['password'],
+ toolbarState: ToolbarState(),
+ tabController: tabController,
+ connToken: params['connToken'],
+ forceRelay: params['forceRelay'],
+ isSharedPassword: params['isSharedPassword'],
+ ),
+ ));
+ _update_remote_count();
+ }
+ tabController.onRemoved = (_, id) => onRemoveId(id);
+ rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ if (!_isScreenRectSet) {
+ Future.delayed(Duration.zero, () {
+ restoreWindowPosition(
+ WindowType.ViewCamera,
+ windowId: windowId(),
+ peerId: tabController.state.value.tabs.isEmpty
+ ? null
+ : tabController.state.value.tabs[0].key,
+ display: _display,
+ );
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final child = Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: DesktopTab(
+ controller: tabController,
+ onWindowCloseButton: handleWindowCloseButton,
+ tail: const AddButton(),
+ selectedBorderColor: MyTheme.accent,
+ pageViewBuilder: (pageView) => pageView,
+ labelGetter: DesktopTab.tablabelGetter,
+ tabBuilder: (key, icon, label, themeConf) => Obx(() {
+ final connectionType = ConnectionTypeState.find(key);
+ if (!connectionType.isValid()) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ icon,
+ label,
+ ],
+ );
+ } else {
+ bool secure =
+ connectionType.secure.value == ConnectionType.strSecure;
+ bool direct =
+ connectionType.direct.value == ConnectionType.strDirect;
+ String msgConn = getConnectionText(
+ secure, direct, connectionType.stream_type.value);
+ var msgFingerprint = '${translate('Fingerprint')}:\n';
+ var fingerprint = FingerprintState.find(key).value;
+ if (fingerprint.isEmpty) {
+ fingerprint = 'N/A';
+ }
+ if (fingerprint.length > 5 * 8) {
+ var first = fingerprint.substring(0, 39);
+ var second = fingerprint.substring(40);
+ msgFingerprint += '$first\n$second';
+ } else {
+ msgFingerprint += fingerprint;
+ }
+
+ final tab = Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ icon,
+ Tooltip(
+ message: '$msgConn\n$msgFingerprint',
+ child: SvgPicture.asset(
+ 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
+ width: themeConf.iconSize,
+ height: themeConf.iconSize,
+ ).paddingOnly(right: 5),
+ ),
+ label,
+ unreadMessageCountBuilder(UnreadChatCountState.find(key))
+ .marginOnly(left: 4),
+ ],
+ );
+
+ return Listener(
+ onPointerDown: (e) {
+ if (e.kind != ui.PointerDeviceKind.mouse) {
+ return;
+ }
+ final viewCameraPage = tabController.state.value.tabs
+ .firstWhere((tab) => tab.key == key)
+ .page as ViewCameraPage;
+ if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue &&
+ e.buttons == 2) {
+ showRightMenu(
+ (CancelFunc cancelFunc) {
+ return _tabMenuBuilder(key, cancelFunc);
+ },
+ target: e.position,
+ );
+ }
+ },
+ child: tab,
+ );
+ }
+ }),
+ ),
+ );
+ final tabWidget = isLinux
+ ? buildVirtualWindowFrame(context, child)
+ : workaroundWindowBorder(
+ context,
+ Obx(() => Container(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: MyTheme.color(context).border!,
+ width: stateGlobal.windowBorderWidth.value),
+ ),
+ child: child,
+ )));
+ return isMacOS || kUseCompatibleUiMode
+ ? tabWidget
+ : Obx(() => SubWindowDragToResizeArea(
+ key: contentKey,
+ child: tabWidget,
+ // Specially configured for a better resize area and remote control.
+ childPadding: kDragToResizeAreaPadding,
+ resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: subWindowManagerEnableResizeEdges,
+ windowId: stateGlobal.windowId,
+ ));
+ }
+
+ // Note: Some dup code to ../widgets/remote_toolbar
+ Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
+ final List> menu = [];
+ const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
+ final viewCameraPage = tabController.state.value.tabs
+ .firstWhere((tab) => tab.key == key)
+ .page as ViewCameraPage;
+ final ffi = viewCameraPage.ffi;
+ final sessionId = ffi.sessionId;
+ final toolbarState = viewCameraPage.toolbarState;
+ menu.addAll([
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Obx(() => Text(
+ translate(
+ toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
+ style: style,
+ )),
+ proc: () {
+ toolbarState.switchShow(sessionId);
+ cancelFunc();
+ },
+ padding: padding,
+ ),
+ ]);
+
+ if (tabController.state.value.tabs.length > 1) {
+ final splitAction = MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Move tab to new window'),
+ style: style,
+ ),
+ proc: () async {
+ await DesktopMultiWindow.invokeMethod(
+ kMainWindowId,
+ kWindowEventMoveTabToNewWindow,
+ '${windowId()},$key,$sessionId,ViewCamera');
+ cancelFunc();
+ },
+ padding: padding,
+ );
+ menu.insert(1, splitAction);
+ }
+
+ menu.addAll([
+ MenuEntryDivider(),
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Copy Fingerprint'),
+ style: style,
+ ),
+ proc: () => onCopyFingerprint(FingerprintState.find(key).value),
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: cancelFunc,
+ ),
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Close'),
+ style: style,
+ ),
+ proc: () {
+ tabController.closeBy(key);
+ cancelFunc();
+ },
+ padding: padding,
+ )
+ ]);
+
+ return mod_menu.PopupMenu(
+ items: menu
+ .map((entry) => entry.build(
+ context,
+ const MenuConfig(
+ commonColor: _MenuTheme.blueColor,
+ height: _MenuTheme.height,
+ dividerHeight: _MenuTheme.dividerHeight,
+ )))
+ .expand((i) => i)
+ .toList(),
+ );
+ }
+
+ void onRemoveId(String id) async {
+ if (tabController.state.value.tabs.isEmpty) {
+ // Keep calling until the window status is hidden.
+ //
+ // Workaround for Windows:
+ // If you click other buttons and close in msgbox within a very short period of time, the close may fail.
+ // `await WindowController.fromWindowId(windowId()).close();`.
+ Future loopCloseWindow() async {
+ int c = 0;
+ final windowController = WindowController.fromWindowId(windowId());
+ while (c < 20 &&
+ tabController.state.value.tabs.isEmpty &&
+ (!await windowController.isHidden())) {
+ await windowController.close();
+ await Future.delayed(Duration(milliseconds: 100));
+ c++;
+ }
+ }
+
+ loopCloseWindow();
+ }
+ ConnectionTypeState.delete(id);
+ _update_remote_count();
+ }
+
+ int windowId() {
+ return widget.params["windowId"];
+ }
+
+ Future handleWindowCloseButton() async {
+ final connLength = tabController.length;
+ if (connLength <= 1) {
+ tabController.clear();
+ return true;
+ } else {
+ final bool res;
+ if (!option2bool(kOptionEnableConfirmClosingTabs,
+ bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
+ res = true;
+ } else {
+ res = await closeConfirmDialog();
+ }
+ if (res) {
+ tabController.clear();
+ }
+ return res;
+ }
+ }
+
+ _update_remote_count() =>
+ RemoteCountState.find().value = tabController.length;
+
+ Future _remoteMethodHandler(call, fromWindowId) async {
+ debugPrint(
+ "[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
+
+ dynamic returnValue;
+ // for simplify, just replace connectionId
+ if (call.method == kWindowEventNewViewCamera) {
+ final args = jsonDecode(call.arguments);
+ final id = args['id'];
+ final sessionId = args['session_id'];
+ final tabWindowId = args['tab_window_id'];
+ final display = args['display'];
+ final displays = args['displays'];
+ final screenRect = parseParamScreenRect(args);
+ final prePeerCount = tabController.length;
+ Future.delayed(Duration.zero, () async {
+ if (stateGlobal.fullscreen.isTrue) {
+ await WindowController.fromWindowId(windowId()).setFullscreen(false);
+ stateGlobal.setFullscreen(false, procWnd: false);
+ }
+ await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
+ WindowType.ViewCamera, display, screenRect);
+ Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
+ await windowOnTop(windowId());
+ });
+ });
+ ConnectionTypeState.init(id);
+ tabController.add(TabInfo(
+ key: id,
+ label: id,
+ selectedIcon: selectedIcon,
+ unselectedIcon: unselectedIcon,
+ onTabCloseButton: () => tabController.closeBy(id),
+ page: ViewCameraPage(
+ key: ValueKey(id),
+ id: id,
+ sessionId: sessionId == null ? null : SessionID(sessionId),
+ tabWindowId: tabWindowId,
+ display: display,
+ displays: displays?.cast(),
+ password: args['password'],
+ toolbarState: ToolbarState(),
+ tabController: tabController,
+ connToken: args['connToken'],
+ forceRelay: args['forceRelay'],
+ isSharedPassword: args['isSharedPassword'],
+ ),
+ ));
+ } else if (call.method == kWindowDisableGrabKeyboard) {
+ // ???
+ } else if (call.method == "onDestroy") {
+ tabController.clear();
+ } else if (call.method == kWindowActionRebuild) {
+ reloadCurrentWindow();
+ } else if (call.method == kWindowEventActiveSession) {
+ final jumpOk = tabController.jumpToByKey(call.arguments);
+ if (jumpOk) {
+ windowOnTop(windowId());
+ }
+ return jumpOk;
+ } else if (call.method == kWindowEventActiveDisplaySession) {
+ final args = jsonDecode(call.arguments);
+ final id = args['id'];
+ final display = args['display'];
+ final jumpOk =
+ tabController.jumpToByKeyAndDisplay(id, display, isCamera: true);
+ if (jumpOk) {
+ windowOnTop(windowId());
+ }
+ return jumpOk;
+ } else if (call.method == kWindowEventGetRemoteList) {
+ return tabController.state.value.tabs
+ .map((e) => e.key)
+ .toList()
+ .join(',');
+ } else if (call.method == kWindowEventGetSessionIdList) {
+ return tabController.state.value.tabs
+ .map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}')
+ .toList()
+ .join(';');
+ } else if (call.method == kWindowEventGetCachedSessionData) {
+ // Ready to show new window and close old tab.
+ final args = jsonDecode(call.arguments);
+ final id = args['id'];
+ final close = args['close'];
+ try {
+ final viewCameraPage = tabController.state.value.tabs
+ .firstWhere((tab) => tab.key == id)
+ .page as ViewCameraPage;
+ returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString();
+ } catch (e) {
+ debugPrint('Failed to get cached session data: $e');
+ }
+ if (close && returnValue != null) {
+ closeSessionOnDispose[id] = false;
+ tabController.closeBy(id);
+ }
+ } else if (call.method == kWindowEventRemoteWindowCoords) {
+ final viewCameraPage =
+ tabController.state.value.selectedTabInfo.page as ViewCameraPage;
+ final ffi = viewCameraPage.ffi;
+ final displayRect = ffi.ffiModel.displaysRect();
+ if (displayRect != null) {
+ final wc = WindowController.fromWindowId(windowId());
+ Rect? frame;
+ try {
+ frame = await wc.getFrame();
+ } catch (e) {
+ debugPrint(
+ "Failed to get frame of window $windowId, it may be hidden");
+ }
+ if (frame != null) {
+ ffi.cursorModel.moveLocal(0, 0);
+ final coords = RemoteWindowCoords(
+ frame,
+ CanvasCoords.fromCanvasModel(ffi.canvasModel),
+ CursorCoords.fromCursorModel(ffi.cursorModel),
+ displayRect);
+ returnValue = jsonEncode(coords.toJson());
+ }
+ }
+ } else if (call.method == kWindowEventSetFullscreen) {
+ stateGlobal.setFullscreen(call.arguments == 'true');
+ }
+ _update_remote_count();
+ return returnValue;
+ }
+}
diff --git a/flutter/lib/desktop/screen/desktop_terminal_screen.dart b/flutter/lib/desktop/screen/desktop_terminal_screen.dart
new file mode 100644
index 00000000000..301489c8650
--- /dev/null
+++ b/flutter/lib/desktop/screen/desktop_terminal_screen.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:provider/provider.dart';
+
+import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart';
+
+class DesktopTerminalScreen extends StatelessWidget {
+ final Map params;
+
+ const DesktopTerminalScreen({Key? key, required this.params})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider.value(value: gFFI.ffiModel),
+ ],
+ child: Scaffold(
+ backgroundColor: isLinux ? Colors.transparent : null,
+ body: TerminalTabPage(
+ params: params,
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/desktop/screen/desktop_view_camera_screen.dart b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart
new file mode 100644
index 00000000000..a845b89d01c
--- /dev/null
+++ b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
+import 'package:provider/provider.dart';
+
+/// multi-tab desktop remote screen
+class DesktopViewCameraScreen extends StatelessWidget {
+ final Map params;
+
+ DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) {
+ bind.mainInitInputSource();
+ stateGlobal.getInputSource(force: true);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider.value(value: gFFI.ffiModel),
+ ChangeNotifierProvider.value(value: gFFI.imageModel),
+ ChangeNotifierProvider.value(value: gFFI.cursorModel),
+ ChangeNotifierProvider.value(value: gFFI.canvasModel),
+ ],
+ child: Scaffold(
+ // Set transparent background for padding the resize area out of the flutter view.
+ // This allows the wallpaper goes through our resize area. (Linux only now).
+ backgroundColor: isLinux ? Colors.transparent : null,
+ body: ViewCameraTabPage(
+ params: params,
+ ),
+ ));
+ }
+}
diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart
index 839ea1a81db..f29908d5198 100644
--- a/flutter/lib/desktop/widgets/remote_toolbar.dart
+++ b/flutter/lib/desktop/widgets/remote_toolbar.dart
@@ -4,6 +4,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
+import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -478,7 +479,10 @@ class _RemoteToolbarState extends State {
state: widget.state,
setFullscreen: _setFullscreen,
));
- toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
+ // Do not show keyboard for camera connection type.
+ if (widget.ffi.connType == ConnType.defaultConn) {
+ toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
+ }
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) {
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
@@ -1043,23 +1047,26 @@ class _DisplayMenuState extends State<_DisplayMenu> {
scrollStyle(),
imageQuality(),
codec(),
- _ResolutionsMenu(
- id: widget.id,
- ffi: widget.ffi,
- screenAdjustor: _screenAdjustor,
- ),
- if (showVirtualDisplayMenu(ffi))
+ if (ffi.connType == ConnType.defaultConn)
+ _ResolutionsMenu(
+ id: widget.id,
+ ffi: widget.ffi,
+ screenAdjustor: _screenAdjustor,
+ ),
+ if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn)
_SubmenuButton(
ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")),
),
- cursorToggles(),
+ if (ffi.connType == ConnType.defaultConn) cursorToggles(),
Divider(),
toggles(),
];
// privacy mode
- if (ffiModel.keyboard && pi.features.privacyMode) {
+ if (ffi.connType == ConnType.defaultConn &&
+ ffiModel.keyboard &&
+ pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
@@ -1085,7 +1092,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
]);
}
}
- menuChildren.add(widget.pluginItem);
+ if (ffi.connType == ConnType.defaultConn) {
+ menuChildren.add(widget.pluginItem);
+ }
return menuChildren;
}
@@ -1495,7 +1504,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
);
}
- TextField _resolutionInput(TextEditingController controller) {
+ Widget _resolutionInput(TextEditingController controller) {
return TextField(
decoration: InputDecoration(
border: InputBorder.none,
@@ -1509,7 +1518,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
controller: controller,
- );
+ ).workaroundFreezeLinuxMint();
}
List _supportedResolutionMenuButtons() => resolutions
@@ -1586,10 +1595,28 @@ class _KeyboardMenu extends StatelessWidget {
viewMode(),
Divider(),
...toolbarToggles(),
+ ...mouseSpeed(),
...mobileActions(),
]);
}
+ mouseSpeed() {
+ final speedWidgets = [];
+ final sessionId = ffi.sessionId;
+ if (isDesktop) {
+ if (ffi.ffiModel.keyboard) {
+ final enabled = !ffi.ffiModel.viewOnly;
+ final trackpad = MenuButton(
+ child: Text(translate('Trackpad speed')).paddingOnly(left: 26.0),
+ onPressed: enabled ? () => trackpadSpeedDialog(sessionId, ffi) : null,
+ ffi: ffi,
+ );
+ speedWidgets.add(trackpad);
+ }
+ }
+ return speedWidgets;
+ }
+
keyboardMode() {
return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index 96ada22c907..c1cc433ad10 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
+import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -51,7 +52,9 @@ enum DesktopTabType {
cm,
remoteScreen,
fileTransfer,
+ viewCamera,
portForward,
+ terminal,
install,
}
@@ -179,11 +182,13 @@ class DesktopTabController {
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
callOnSelected: callOnSelected);
- bool jumpToByKeyAndDisplay(String key, int display) {
+ bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) {
for (int i = 0; i < state.value.tabs.length; i++) {
final tab = state.value.tabs[i];
if (tab.key == key) {
- final ffi = (tab.page as RemotePage).ffi;
+ final ffi = isCamera
+ ? (tab.page as ViewCameraPage).ffi
+ : (tab.page as RemotePage).ffi;
if (ffi.ffiModel.pi.currentDisplay == display) {
return jumpTo(i, callOnSelected: true);
}
@@ -647,7 +652,9 @@ class _DesktopTabState extends State
controller.state.value.scrollController;
if (!sc.canScroll) return;
_scrollDebounce.call(() {
- sc.animateTo(sc.offset + e.scrollDelta.dy,
+ double adjust = 2.5;
+ sc.animateTo(
+ sc.offset + e.scrollDelta.dy * adjust,
duration: Duration(milliseconds: 200),
curve: Curves.ease);
});
@@ -725,6 +732,7 @@ class WindowActionPanelState extends State {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
+ widget.tabController.tabType == DesktopTabType.viewCamera ||
widget.tabController.tabType == DesktopTabType.portForward ||
widget.tabController.tabType == DesktopTabType.cm);
}
diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart
new file mode 100644
index 00000000000..93f661b7b7d
--- /dev/null
+++ b/flutter/lib/desktop/widgets/update_progress.dart
@@ -0,0 +1,267 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+final _isExtracting = false.obs;
+
+void handleUpdate(String releasePageUrl) {
+ _isExtracting.value = false;
+ String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
+ String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
+ final String downloadFile =
+ bind.mainGetCommonSync(key: 'download-file-$version');
+ if (downloadFile.startsWith('error:')) {
+ final error = downloadFile.replaceFirst('error:', '');
+ msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error,
+ releasePageUrl, gFFI.dialogManager);
+ return;
+ }
+ downloadUrl = '$downloadUrl/$downloadFile';
+
+ SimpleWrapper downloadId = SimpleWrapper('');
+ SimpleWrapper onCanceled = SimpleWrapper(() {});
+ gFFI.dialogManager.dismissAll();
+ gFFI.dialogManager.show((setState, close, context) {
+ return CustomAlertDialog(
+ title: Obx(() => Text(translate(_isExtracting.isTrue
+ ? 'Preparing for installation ...'
+ : 'Downloading {$appName}'))),
+ content:
+ UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
+ .marginSymmetric(horizontal: 8)
+ .paddingOnly(top: 12),
+ actions: [
+ if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async {
+ onCanceled.value();
+ await bind.mainSetCommon(
+ key: 'cancel-downloader', value: downloadId.value);
+ // Wait for the downloader to be removed.
+ for (int i = 0; i < 10; i++) {
+ await Future.delayed(const Duration(milliseconds: 300));
+ final isCanceled = 'error:Downloader not found' ==
+ await bind.mainGetCommon(
+ key: 'download-data-${downloadId.value}');
+ if (isCanceled) {
+ break;
+ }
+ }
+ close();
+ }, isOutline: true),
+ ]);
+ });
+}
+
+class UpdateProgress extends StatefulWidget {
+ final String releasePageUrl;
+ final String downloadUrl;
+ final SimpleWrapper downloadId;
+ final SimpleWrapper onCanceled;
+ UpdateProgress(
+ this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled,
+ {Key? key})
+ : super(key: key);
+
+ @override
+ State createState() => UpdateProgressState();
+}
+
+class UpdateProgressState extends State {
+ Timer? _timer;
+ int? _totalSize;
+ int _downloadedSize = 0;
+ int _getDataFailedCount = 0;
+ final String _eventKeyDownloadNewVersion = 'download-new-version';
+ final String _eventKeyExtractUpdateDmg = 'extract-update-dmg';
+
+ @override
+ void initState() {
+ super.initState();
+ widget.onCanceled.value = () {
+ cancelQueryTimer();
+ };
+ platformFFI.registerEventHandler(_eventKeyDownloadNewVersion,
+ _eventKeyDownloadNewVersion, handleDownloadNewVersion,
+ replace: true);
+ bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
+ if (isMacOS) {
+ platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg,
+ _eventKeyExtractUpdateDmg, handleExtractUpdateDmg,
+ replace: true);
+ }
+ }
+
+ @override
+ void dispose() {
+ cancelQueryTimer();
+ platformFFI.unregisterEventHandler(
+ _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
+ if (isMacOS) {
+ platformFFI.unregisterEventHandler(
+ _eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg);
+ }
+ super.dispose();
+ }
+
+ void cancelQueryTimer() {
+ _timer?.cancel();
+ _timer = null;
+ }
+
+ Future handleDownloadNewVersion(Map evt) async {
+ if (evt.containsKey('id')) {
+ widget.downloadId.value = evt['id'] as String;
+ _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
+ _updateDownloadData();
+ });
+ } else {
+ if (evt.containsKey('error')) {
+ _onError(evt['error'] as String);
+ } else {
+ // unreachable
+ _onError('$evt');
+ }
+ }
+ }
+
+ // `isExtractDmg` is true when handling extract-update-dmg event.
+ // It's a rare case that the dmg file is corrupted and cannot be extracted.
+ void _onError(String error, {bool isExtractDmg = false}) {
+ cancelQueryTimer();
+
+ debugPrint(
+ '${isExtractDmg ? "Extract" : "Download"} new version error: $error');
+ final msgBoxType = 'custom-nocancel-nook-hasclose';
+ final msgBoxTitle = 'Error';
+ final msgBoxText = 'download-new-version-failed-tip';
+ final dialogManager = gFFI.dialogManager;
+
+ close() {
+ dialogManager.dismissAll();
+ }
+
+ jumplink() {
+ launchUrl(Uri.parse(widget.releasePageUrl));
+ dialogManager.dismissAll();
+ }
+
+ retry() {
+ dialogManager.dismissAll();
+ handleUpdate(widget.releasePageUrl);
+ }
+
+ final List buttons = [
+ dialogButton('Download', onPressed: jumplink),
+ if (!isExtractDmg) dialogButton('Retry', onPressed: retry),
+ dialogButton('Close', onPressed: close),
+ ];
+ dialogManager.dismissAll();
+ dialogManager.show(
+ (setState, close, context) => CustomAlertDialog(
+ title: null,
+ content: SelectionArea(
+ child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
+ actions: buttons,
+ ),
+ tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
+ );
+ }
+
+ void _updateDownloadData() {
+ String err = '';
+ String downloadData =
+ bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}');
+ if (downloadData.startsWith('error:')) {
+ err = downloadData.substring('error:'.length);
+ } else {
+ try {
+ jsonDecode(downloadData).forEach((key, value) {
+ if (key == 'total_size') {
+ if (value != null && value is int) {
+ _totalSize = value;
+ }
+ } else if (key == 'downloaded_size') {
+ _downloadedSize = value as int;
+ } else if (key == 'error') {
+ if (value != null) {
+ err = value.toString();
+ }
+ }
+ });
+ } catch (e) {
+ _getDataFailedCount += 1;
+ debugPrint(
+ 'Failed to get download data ${widget.downloadUrl}, error $e');
+ if (_getDataFailedCount > 3) {
+ err = e.toString();
+ }
+ }
+ }
+ if (err != '') {
+ _onError(err);
+ } else {
+ if (_totalSize != null && _downloadedSize >= _totalSize!) {
+ cancelQueryTimer();
+ bind.mainSetCommon(
+ key: 'remove-downloader', value: widget.downloadId.value);
+ if (_totalSize == 0) {
+ _onError('The download file size is 0.');
+ } else {
+ setState(() {});
+ if (isMacOS) {
+ bind.mainSetCommon(
+ key: 'extract-update-dmg', value: widget.downloadUrl);
+ _isExtracting.value = true;
+ } else {
+ updateMsgBox();
+ }
+ }
+ } else {
+ setState(() {});
+ }
+ }
+ }
+
+ void updateMsgBox() {
+ msgBox(
+ gFFI.sessionId,
+ 'custom-nocancel',
+ '{$appName} Update',
+ '{$appName}-to-update-tip',
+ '',
+ gFFI.dialogManager,
+ onSubmit: () {
+ debugPrint('Downloaded, update to new version now');
+ bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
+ },
+ submitTimeout: 5,
+ );
+ }
+
+ Future handleExtractUpdateDmg(Map evt) async {
+ _isExtracting.value = false;
+ if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) {
+ _onError(evt['err'] as String, isExtractDmg: true);
+ } else {
+ updateMsgBox();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ getValue() => _totalSize == null
+ ? 0.0
+ : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
+ return LinearProgressIndicator(
+ value: _isExtracting.isTrue ? null : getValue(),
+ minHeight: 20,
+ borderRadius: BorderRadius.circular(5),
+ backgroundColor: Colors.grey[300],
+ valueColor: const AlwaysStoppedAnimation(Colors.blue),
+ );
+ }
+}
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index 3032a2321f0..80a3bff894a 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -11,8 +11,10 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
+import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
+import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -76,6 +78,13 @@ Future main(List args) async {
kAppTypeDesktopFileTransfer,
);
break;
+ case WindowType.ViewCamera:
+ desktopType = DesktopType.viewCamera;
+ runMultiWindow(
+ argument,
+ kAppTypeDesktopViewCamera,
+ );
+ break;
case WindowType.PortForward:
desktopType = DesktopType.portForward;
runMultiWindow(
@@ -83,6 +92,12 @@ Future main(List args) async {
kAppTypeDesktopPortForward,
);
break;
+ case WindowType.Terminal:
+ desktopType = DesktopType.terminal;
+ runMultiWindow(
+ argument,
+ kAppTypeDesktopTerminal,
+ );
default:
break;
}
@@ -133,7 +148,8 @@ void runMainApp(bool startService) async {
runApp(App());
// Set window option.
- WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
+ WindowOptions windowOptions =
+ getHiddenTitleBarWindowOptions(isMainWindow: true);
windowManager.waitUntilReadyToShow(windowOptions, () async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);
@@ -191,11 +207,22 @@ void runMultiWindow(
params: argument,
);
break;
+ case kAppTypeDesktopViewCamera:
+ draggablePositions.load();
+ widget = DesktopViewCameraScreen(
+ params: argument,
+ );
+ break;
case kAppTypeDesktopPortForward:
widget = DesktopPortForwardScreen(
params: argument,
);
break;
+ case kAppTypeDesktopTerminal:
+ widget = DesktopTerminalScreen(
+ params: argument,
+ );
+ break;
default:
// no such appType
exit(0);
@@ -226,9 +253,25 @@ void runMultiWindow(
await restoreWindowPosition(WindowType.FileTransfer,
windowId: kWindowId!);
break;
+ case kAppTypeDesktopViewCamera:
+ // If screen rect is set, the window will be moved to the target screen and then set fullscreen.
+ if (argument['screen_rect'] == null) {
+ // display can be used to control the offset of the window.
+ await restoreWindowPosition(
+ WindowType.ViewCamera,
+ windowId: kWindowId!,
+ peerId: argument['id'] as String?,
+ // FIXME: fix display index.
+ display: argument['display'] as int?,
+ );
+ }
+ break;
case kAppTypeDesktopPortForward:
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
break;
+ case kAppTypeDesktopTerminal:
+ await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!);
+ break;
default:
// no such appType
exit(0);
@@ -354,7 +397,10 @@ void runInstallPage() async {
}
WindowOptions getHiddenTitleBarWindowOptions(
- {Size? size, bool center = false, bool? alwaysOnTop}) {
+ {bool isMainWindow = false,
+ Size? size,
+ bool center = false,
+ bool? alwaysOnTop}) {
var defaultTitleBarStyle = TitleBarStyle.hidden;
// we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) {
@@ -363,7 +409,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
return WindowOptions(
size: size,
center: center,
- backgroundColor: Colors.transparent,
+ backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent,
skipTaskbar: false,
titleBarStyle: defaultTitleBarStyle,
alwaysOnTop: alwaysOnTop,
@@ -485,9 +531,10 @@ class _AppState extends State with WidgetsBindingObserver {
child = keyListenerBuilder(context, child);
}
if (isLinux) {
- child = buildVirtualWindowFrame(context, child);
+ return buildVirtualWindowFrame(context, child);
+ } else {
+ return workaroundWindowBorder(context, child);
}
- return child;
},
),
);
diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart
index 49e3b2c9107..07aaaef8cdf 100644
--- a/flutter/lib/mobile/pages/connection_page.dart
+++ b/flutter/lib/mobile/pages/connection_page.dart
@@ -41,10 +41,11 @@ class _ConnectionPageState extends State {
final _idController = IDTextEditingController();
final RxBool _idEmpty = true.obs;
- List peers = [];
+ final FocusNode _idFocusNode = FocusNode();
+ final TextEditingController _idEditingController = TextEditingController();
+
+ final AllPeersLoader _allPeersLoader = AllPeersLoader();
- bool isPeersLoading = false;
- bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription;
// https://github.com/flutter/flutter/issues/157244
@@ -61,6 +62,8 @@ class _ConnectionPageState extends State {
@override
void initState() {
super.initState();
+ _allPeersLoader.init(setState);
+ _idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@@ -71,6 +74,7 @@ class _ConnectionPageState extends State {
}
});
}
+ Get.put(_idEditingController);
}
@override
@@ -80,7 +84,7 @@ class _ConnectionPageState extends State {
slivers: [
SliverList(
delegate: SliverChildListDelegate([
- if (!bind.isCustomClient())
+ if (!bind.isCustomClient() && !isIOS)
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
_buildRemoteIDTextField(),
])),
@@ -99,6 +103,20 @@ class _ConnectionPageState extends State {
connect(context, id);
}
+ void onFocusChanged() {
+ _idEmpty.value = _idEditingController.text.isEmpty;
+ if (_idFocusNode.hasFocus) {
+ if (_allPeersLoader.needLoad) {
+ _allPeersLoader.getAllPeers();
+ }
+
+ final textLength = _idEditingController.value.text.length;
+ // Select all to facilitate removing text, just following the behavior of address input of chrome.
+ _idEditingController.selection =
+ TextSelection(baseOffset: 0, extentOffset: textLength);
+ }
+ }
+
/// UI for software update.
/// If _updateUrl] is not empty, shows a button to update the software.
Widget _buildUpdateUI(String updateUrl) {
@@ -107,7 +125,7 @@ class _ConnectionPageState extends State {
: InkWell(
onTap: () async {
final url = 'https://rustdesk.com/download';
- // https://pub.dev/packages/url_launcher#configuration
+ // https://pub.dev/packages/url_launcher#configuration
// https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
//
// `await launchUrl(Uri.parse(url))` can also run if skip
@@ -115,9 +133,7 @@ class _ConnectionPageState extends State {
// 2. `` in AndroidManifest.xml
//
// But it is better to add the check.
- if (await canLaunchUrl(Uri.parse(url))) {
- await launchUrl(Uri.parse(url));
- }
+ await launchUrl(Uri.parse(url));
},
child: Container(
alignment: AlignmentDirectional.center,
@@ -129,18 +145,6 @@ class _ConnectionPageState extends State {
color: Colors.white, fontWeight: FontWeight.bold))));
}
- Future _fetchPeers() async {
- setState(() {
- isPeersLoading = true;
- });
- await Future.delayed(Duration(milliseconds: 100));
- peers = await getAllPeers();
- setState(() {
- isPeersLoading = false;
- isPeersLoaded = true;
- });
- }
-
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget _buildRemoteIDTextField() {
@@ -158,11 +162,12 @@ class _ConnectionPageState extends State {
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
- child: Autocomplete(
+ child: RawAutocomplete(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable