Merge branch 'rustdesk:master' into master

This commit is contained in:
rainerosion 2024-04-29 01:25:16 +08:00 committed by GitHub
commit fa71d6fca1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 2799 additions and 561 deletions

View File

@ -32,25 +32,38 @@ env:
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs: jobs:
build-for-macOS-arm64: build-rustdesk-ios:
name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64] runs-on: [self-hosted, macOS, ARM64]
strategy:
fail-fast: false
steps: steps:
#- name: Import the codesign cert - name: Export GitHub Actions cache environment variables
# if: env.MACOS_P12_BASE64 != null uses: actions/github-script@v6
# uses: apple-actions/import-codesign-certs@v1 with:
# continue-on-error: true script: |
# with: core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
# keychain: rustdesk - name: Checkout source code
uses: actions/checkout@v3
#- name: Check sign and import sign key
# if: env.MACOS_P12_BASE64 != null # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
# run: |
# security default-keychain -s rustdesk.keychain - name: Install flutter rust bridge deps
# security find-identity -v
- name: Run
shell: bash shell: bash
run: | run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib
run: |
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

View File

@ -18,12 +18,10 @@ env:
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
TAG_NAME: "${{ inputs.upload-tag }}" TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19 # vcpkg version: 2024.03.25
# for multiarch gcc compatibility VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
VERSION: "1.2.4" VERSION: "1.2.4"
NDK_VERSION: "r26b" NDK_VERSION: "r26b"
#signing keys env variable checks #signing keys env variable checks
@ -321,7 +319,9 @@ jobs:
./SignOutput/rustdesk-*.exe ./SignOutput/rustdesk-*.exe
./rustdesk-*.tar.gz ./rustdesk-*.tar.gz
build-for-macOS-arm64: build-for-macOS-arm64-selfhost:
# use build-for-macOS instead
if: false
name: build-for-macOS-arm64 name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64] runs-on: [self-hosted, macOS, ARM64]
steps: steps:
@ -392,6 +392,154 @@ jobs:
files: | files: |
rustdesk*-aarch64.dmg rustdesk*-aarch64.dmg
build-rustdesk-ios:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-apple-ios,
os: macos-13,
extra-build-features: "",
}
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: Install dependencies
run: |
brew install nasm
- name: Checkout source code
uses: actions/checkout@v3
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib
run: |
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]
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@v3
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib
run: |
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- 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
# 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-for-macOS: build-for-macOS:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
@ -401,10 +549,16 @@ jobs:
job: job:
- { - {
target: x86_64-apple-darwin, target: x86_64-apple-darwin,
os: macos-latest, os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "", extra-build-args: "",
arch: x86_64, arch: x86_64,
} }
- {
target: aarch64-apple-darwin,
os: macos-latest,
extra-build-args: "",
arch: aarch64,
}
steps: steps:
- name: Export GitHub Actions cache environment variables - name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6 uses: actions/github-script@v6
@ -506,14 +660,14 @@ jobs:
CREATE_DMG="$(command -v create-dmg)" CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")" CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$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 }}-x64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Upload unsigned macOS app - name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master uses: actions/upload-artifact@master
with: with:
name: rustdesk-unsigned-macos-x64 name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
path: rustdesk-${{ env.VERSION }}-x64.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
- name: Codesign app and create signed dmg - name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true' if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
@ -552,20 +706,20 @@ jobs:
publish_unsigned_mac: publish_unsigned_mac:
needs: needs:
- build-for-macOS - build-for-macOS
- build-for-macOS-arm64 #- build-for-macOS-arm64
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ inputs.upload-artifact }} if: ${{ inputs.upload-artifact }}
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@master uses: actions/download-artifact@master
with: with:
name: rustdesk-unsigned-macos-x64 name: rustdesk-unsigned-macos-x86_64
path: ./ path: ./
- name: Download Artifacts - name: Download Artifacts
uses: actions/download-artifact@master uses: actions/download-artifact@master
with: with:
name: rustdesk-unsigned-macos-arm64 name: rustdesk-unsigned-macos-aarch64
path: ./ path: ./
- name: Combine unsigned macos app - name: Combine unsigned macos app
@ -582,97 +736,6 @@ jobs:
generate-bridge-linux: generate-bridge-linux:
uses: ./.github/workflows/bridge.yml uses: ./.github/workflows/bridge.yml
build-rustdesk-ios:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-apple-ios,
os: macos-latest,
extra-build-features: "",
}
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: Install dependencies
run: |
brew install nasm
- name: Checkout source code
uses: actions/checkout@v3
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib
run: |
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-android: build-rustdesk-android:
needs: [generate-bridge-linux] needs: [generate-bridge-linux]
name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
@ -1545,7 +1608,7 @@ jobs:
git clone https://github.com/sony/flutter-elinux.git || true git clone https://github.com/sony/flutter-elinux.git || true
pushd flutter-elinux pushd flutter-elinux
git fetch git fetch
git reset --hard ${{ env.FLUTTER_ELINUX_COMMIT_ID }} git reset --hard ${{ env.FLUTTER_VERSION }}
popd popd
- uses: rustdesk-org/run-on-arch-action@amd64-support - uses: rustdesk-org/run-on-arch-action@amd64-support

208
Cargo.lock generated
View File

@ -300,9 +300,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.5" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [ dependencies = [
"flate2", "flate2",
"futures-core", "futures-core",
@ -517,6 +517,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.6.0" version = "1.6.0"
@ -1825,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
@ -2825,9 +2831,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.24" version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -2876,6 +2882,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backtrace", "backtrace",
"base64 0.22.0",
"bytes", "bytes",
"chrono", "chrono",
"confy", "confy",
@ -2887,6 +2894,7 @@ dependencies = [
"flexi_logger", "flexi_logger",
"futures", "futures",
"futures-util", "futures-util",
"httparse",
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
@ -2898,6 +2906,8 @@ dependencies = [
"quinn", "quinn",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"rustls-pki-types",
"rustls-platform-verifier",
"serde 1.0.190", "serde 1.0.190",
"serde_derive", "serde_derive",
"serde_json 1.0.107", "serde_json 1.0.107",
@ -2906,9 +2916,12 @@ dependencies = [
"sysinfo", "sysinfo",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-socks", "tokio-native-tls",
"tokio-rustls 0.26.0",
"tokio-socks 0.5.1-2",
"tokio-util", "tokio-util",
"toml 0.7.8", "toml 0.7.8",
"url",
"uuid", "uuid",
"winapi 0.3.9", "winapi 0.3.9",
"zstd 0.13.0", "zstd 0.13.0",
@ -2985,9 +2998,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -2996,9 +3009,9 @@ dependencies = [
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.5" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@ -3025,8 +3038,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hwcodec" name = "hwcodec"
version = "0.3.3" version = "0.4.3"
source = "git+https://github.com/21pages/hwcodec#eeebf980d4eb41daaf05090b097d5a59d688d3d8" source = "git+https://github.com/21pages/hwcodec#db7c2d4afcb4947bfb452213ef7e9ba647578b43"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@ -3038,9 +3051,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.27" version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -3053,7 +3066,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa 1.0.9", "itoa 1.0.9",
"pin-project-lite", "pin-project-lite",
"socket2 0.4.10", "socket2 0.5.5",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -3071,7 +3084,7 @@ dependencies = [
"hyper", "hyper",
"rustls 0.21.10", "rustls 0.21.10",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls 0.24.1",
] ]
[[package]] [[package]]
@ -4565,7 +4578,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
dependencies = [ dependencies = [
"base64", "base64 0.21.5",
"indexmap 1.9.3", "indexmap 1.9.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
@ -4838,7 +4851,7 @@ dependencies = [
"ring 0.16.20", "ring 0.16.20",
"rustc-hash", "rustc-hash",
"rustls 0.20.9", "rustls 0.20.9",
"rustls-native-certs", "rustls-native-certs 0.6.3",
"slab", "slab",
"thiserror", "thiserror",
"tinyvec", "tinyvec",
@ -5173,10 +5186,10 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.23" version = "0.11.23"
source = "git+https://github.com/rustdesk-org/reqwest" source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64", "base64 0.21.5",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -5196,8 +5209,8 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.21.10", "rustls 0.21.10",
"rustls-native-certs", "rustls-native-certs 0.6.3",
"rustls-pemfile", "rustls-pemfile 1.0.3",
"serde 1.0.190", "serde 1.0.190",
"serde_json 1.0.107", "serde_json 1.0.107",
"serde_urlencoded", "serde_urlencoded",
@ -5205,14 +5218,15 @@ dependencies = [
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls 0.24.1",
"tokio-socks 0.5.1",
"tokio-util", "tokio-util",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 0.25.4",
"winreg 0.50.0", "winreg 0.50.0",
] ]
@ -5358,7 +5372,6 @@ dependencies = [
"arboard", "arboard",
"async-process", "async-process",
"async-trait", "async-trait",
"base64",
"bytes", "bytes",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -5519,10 +5532,25 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [ dependencies = [
"log", "log",
"ring 0.17.5", "ring 0.17.5",
"rustls-webpki", "rustls-webpki 0.101.7",
"sct", "sct",
] ]
[[package]]
name = "rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
dependencies = [
"log",
"once_cell",
"ring 0.17.5",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.6.3" version = "0.6.3"
@ -5530,7 +5558,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pemfile", "rustls-pemfile 1.0.3",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework",
] ]
@ -5541,9 +5582,52 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [ dependencies = [
"base64", "base64 0.21.5",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.22.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-platform-verifier"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d"
dependencies = [
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4",
"jni 0.19.0",
"log",
"once_cell",
"rustls 0.23.4",
"rustls-native-certs 0.7.0",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.2",
"security-framework",
"security-framework-sys",
"webpki-roots 0.26.1",
"winapi 0.3.9",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.7" version = "0.101.7"
@ -5554,6 +5638,17 @@ dependencies = [
"untrusted 0.9.0", "untrusted 0.9.0",
] ]
[[package]]
name = "rustls-webpki"
version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"ring 0.17.5",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.14" version = "1.0.14"
@ -5666,22 +5761,23 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4", "core-foundation-sys 0.8.4",
"libc", "libc",
"num-bigint",
"security-framework-sys", "security-framework-sys",
] ]
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.9.1" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [ dependencies = [
"core-foundation-sys 0.8.4", "core-foundation-sys 0.8.4",
"libc", "libc",
@ -6404,6 +6500,17 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.4",
"rustls-pki-types",
"tokio",
]
[[package]] [[package]]
name = "tokio-socks" name = "tokio-socks"
version = "0.5.1-2" version = "0.5.1-2"
@ -6420,6 +6527,18 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "tokio-socks"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
dependencies = [
"either",
"futures-util",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.10" version = "0.7.10"
@ -6637,9 +6756,9 @@ dependencies = [
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
@ -7081,9 +7200,18 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.3" version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "weezl" name = "weezl"
@ -7856,6 +7984,12 @@ dependencies = [
"syn 2.0.55", "syn 2.0.55",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]] [[package]]
name = "zip" name = "zip"
version = "0.6.6" version = "0.6.6"

View File

@ -65,7 +65,6 @@ samplerate = { version = "0.2", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
clap = "4.2" clap = "4.2"
rpassword = "7.2" rpassword = "7.2"
base64 = "0.21"
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14" default-net = "0.14"
@ -145,10 +144,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }

View File

@ -54,6 +54,8 @@ AppDir:
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme

View File

@ -57,6 +57,8 @@ AppDir:
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme

View File

@ -139,4 +139,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
COCOAPODS: 1.12.1 COCOAPODS: 1.15.2

View File

@ -159,7 +159,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -2978,16 +2978,16 @@ Future<bool> setServerConfig(
} }
// id // id
if (config.idServer.isNotEmpty && errMsgs != null) { if (config.idServer.isNotEmpty && errMsgs != null) {
errMsgs[0].value = errMsgs[0].value = translate(await bind.mainTestIfValidServer(
translate(await bind.mainTestIfValidServer(server: config.idServer)); server: config.idServer, testWithProxy: true));
if (errMsgs[0].isNotEmpty) { if (errMsgs[0].isNotEmpty) {
return false; return false;
} }
} }
// relay // relay
if (config.relayServer.isNotEmpty && errMsgs != null) { if (config.relayServer.isNotEmpty && errMsgs != null) {
errMsgs[1].value = errMsgs[1].value = translate(await bind.mainTestIfValidServer(
translate(await bind.mainTestIfValidServer(server: config.relayServer)); server: config.relayServer, testWithProxy: true));
if (errMsgs[1].isNotEmpty) { if (errMsgs[1].isNotEmpty) {
return false; return false;
} }

View File

@ -168,6 +168,29 @@ class ShowRemoteCursorState {
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
class ShowRemoteCursorLockState {
static String tag(String id) => 'show_remote_cursor_lock_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class KeyboardEnabledState { class KeyboardEnabledState {
static String tag(String id) => 'keyboard_enabled_$id'; static String tag(String id) => 'keyboard_enabled_$id';
@ -315,6 +338,7 @@ initSharedStates(String id) {
CurrentDisplayState.init(id); CurrentDisplayState.init(id);
KeyboardEnabledState.init(id); KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id); ShowRemoteCursorState.init(id);
ShowRemoteCursorLockState.init(id);
RemoteCursorMovedState.init(id); RemoteCursorMovedState.init(id);
FingerprintState.init(id); FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false); PeerBoolOption.init(id, 'zoom-cursor', () => false);
@ -327,6 +351,7 @@ removeSharedStates(String id) {
BlockInputState.delete(id); BlockInputState.delete(id);
CurrentDisplayState.delete(id); CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id); ShowRemoteCursorState.delete(id);
ShowRemoteCursorLockState.delete(id);
KeyboardEnabledState.delete(id); KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id); RemoteCursorMovedState.delete(id);
FingerprintState.delete(id); FingerprintState.delete(id);

View File

@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
} }
@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
if (isDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
inputModel.sendMouse('down', MouseButtons.left); inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else { } else {
@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
onOneFingerPanEnd(DragEndDetails d) { onOneFingerPanEnd(DragEndDetails d) {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);
} }

View File

@ -212,6 +212,8 @@ List<(String, String)> otherDefaultSettings() {
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'), if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
('Show remote cursor', 'show_remote_cursor'), ('Show remote cursor', 'show_remote_cursor'),
('Follow remote cursor', 'follow_remote_cursor'),
('Follow remote window focus', 'follow_remote_window'),
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
('Show quality monitor', 'show_quality_monitor'), ('Show quality monitor', 'show_quality_monitor'),
('Mute', 'disable_audio'), ('Mute', 'disable_audio'),

View File

@ -371,7 +371,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
]; ];
} }
Future<List<TToggleMenu>> toolbarDisplayToggle( Future<List<TToggleMenu>> toolbarCursor(
BuildContext context, String id, FFI ffi) async { BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = []; List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
@ -384,12 +384,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
!ffi.canvasModel.cursorEmbedded && !ffi.canvasModel.cursorEmbedded &&
!pi.isWayland) { !pi.isWayland) {
final state = ShowRemoteCursorState.find(id); final state = ShowRemoteCursorState.find(id);
final lockState = ShowRemoteCursorLockState.find(id);
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor'; final option = 'show-remote-cursor';
if (pi.currentDisplay == kAllDisplayValue ||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
lockState.value = false;
}
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Show remote cursor')), child: Text(translate('Show remote cursor')),
value: state.value, value: state.value,
onChanged: enabled onChanged: enabled && !lockState.value
? (value) async { ? (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
@ -399,6 +404,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
} }
: null)); : null));
} }
// follow remote cursor
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-cursor';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
final showCursorOption = 'show-remote-cursor';
final showCursorState = ShowRemoteCursorState.find(id);
final showCursorLockState = ShowRemoteCursorLockState.find(id);
final showCursorEnabled = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
showCursorLockState.value = value;
if (value && !showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
v.add(TToggleMenu(
child: Text(translate('Follow remote cursor')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
showCursorLockState.value = value;
if (!showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
}));
}
// follow remote window focus
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-window';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu(
child: Text(translate('Follow remote window focus')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
}));
}
// zoom cursor // zoom cursor
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
if (!isMobile && if (!isMobile &&
@ -417,6 +483,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
)); ));
} }
return v;
}
Future<List<TToggleMenu>> toolbarDisplayToggle(
BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
// show quality monitor // show quality monitor
final option = 'show-quality-monitor'; final option = 'show-quality-monitor';
v.add(TToggleMenu( v.add(TToggleMenu(

View File

@ -60,6 +60,7 @@ const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";

View File

@ -153,7 +153,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
size: 22, size: 22,
), ),
), ),
onTap: () => DesktopSettingPage.switch2page(0), onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value, onHover: (value) => _editHover.value = value,
), ),
), ),
@ -347,7 +353,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
), ),
), ),
onTap: () => DesktopSettingPage.switch2page(0), onTap: () => DesktopSettingPage.switch2page(
SettingsTabKey.safety),
onHover: (value) => editHover.value = value, onHover: (value) => editHover.value = value,
), ),
], ],
@ -774,6 +781,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession( await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect); windowId, peerId, display, displayCount, screenRect);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
return jsonEncode(
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
}
} }
}); });
_uniLinksSubscription = listenUniLinks(); _uniLinksSubscription = listenUniLinks();

View File

@ -36,34 +36,57 @@ const double _kTitleFontSize = 20;
const double _kContentFontSize = 15; const double _kContentFontSize = 15;
const Color _accentColor = MyTheme.accent; const Color _accentColor = MyTheme.accent;
const String _kSettingPageControllerTag = 'settingPageController'; const String _kSettingPageControllerTag = 'settingPageController';
const String _kSettingPageIndexTag = 'settingPageIndex'; const String _kSettingPageTabKeyTag = 'settingPageTabKey';
const int _kPageCount = 6;
class _TabInfo { class _TabInfo {
late final SettingsTabKey key;
late final String label; late final String label;
late final IconData unselected; late final IconData unselected;
late final IconData selected; late final IconData selected;
_TabInfo(this.label, this.unselected, this.selected); _TabInfo(this.key, this.label, this.unselected, this.selected);
}
enum SettingsTabKey {
general,
safety,
network,
display,
plugin,
account,
about,
} }
class DesktopSettingPage extends StatefulWidget { class DesktopSettingPage extends StatefulWidget {
final int initialPage; final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
SettingsTabKey.about,
];
const DesktopSettingPage({Key? key, required this.initialPage}) DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
: super(key: key);
@override @override
State<DesktopSettingPage> createState() => _DesktopSettingPageState(); State<DesktopSettingPage> createState() => _DesktopSettingPageState();
static void switch2page(int page) { static void switch2page(SettingsTabKey page) {
if (page >= _kPageCount) return;
try { try {
int index = tabKeys.indexOf(page);
if (index == -1) {
return;
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) { if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page); DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag); PageController controller = Get.find(tag: _kSettingPageControllerTag);
RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag); Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
selectedIndex.value = page; selected.value = page;
controller.jumpToPage(page); controller.jumpToPage(index);
} else { } else {
DesktopTabPage.onAddSetting(initialPage: page); DesktopTabPage.onAddSetting(initialPage: page);
} }
@ -76,7 +99,7 @@ class DesktopSettingPage extends StatefulWidget {
class _DesktopSettingPageState extends State<DesktopSettingPage> class _DesktopSettingPageState extends State<DesktopSettingPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late PageController controller; late PageController controller;
late RxInt selectedIndex; late Rx<SettingsTabKey> selectedTab;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -84,14 +107,20 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedIndex = var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
(widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; if (initialIndex == -1) {
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag); initialIndex = 0;
controller = PageController(initialPage: widget.initialPage); }
selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
controller = PageController(initialPage: initialIndex);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag); Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() { controller.addListener(() {
if (controller.page != null) { if (controller.page != null) {
selectedIndex.value = controller.page!.toInt(); int page = controller.page!.toInt();
if (page < DesktopSettingPage.tabKeys.length) {
selectedTab.value = DesktopSettingPage.tabKeys[page];
}
} }
}); });
} }
@ -100,26 +129,43 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
void dispose() { void dispose() {
super.dispose(); super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag); Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageIndexTag); Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
} }
List<_TabInfo> _settingTabs() { List<_TabInfo> _settingTabs() {
final List<_TabInfo> settingTabs = <_TabInfo>[ final List<_TabInfo> settingTabs = <_TabInfo>[];
_TabInfo('General', Icons.settings_outlined, Icons.settings), for (final tab in DesktopSettingPage.tabKeys) {
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) switch (tab) {
_TabInfo('Security', Icons.enhanced_encryption_outlined, case SettingsTabKey.general:
Icons.enhanced_encryption), settingTabs.add(_TabInfo(
if (!bind.isDisableSettings()) tab, 'General', Icons.settings_outlined, Icons.settings));
_TabInfo('Network', Icons.link_outlined, Icons.link), break;
if (!bind.isIncomingOnly()) case SettingsTabKey.safety:
_TabInfo( settingTabs.add(_TabInfo(tab, 'Security',
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows), Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) break;
_TabInfo('Plugin', Icons.extension_outlined, Icons.extension), case SettingsTabKey.network:
if (!bind.isDisableAccount()) settingTabs
_TabInfo('Account', Icons.person_outline, Icons.person), .add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
_TabInfo('About', Icons.info_outline, Icons.info) break;
]; case SettingsTabKey.display:
settingTabs.add(_TabInfo(tab, 'Display',
Icons.desktop_windows_outlined, Icons.desktop_windows));
break;
case SettingsTabKey.plugin:
settingTabs.add(_TabInfo(
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
break;
case SettingsTabKey.account:
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break;
case SettingsTabKey.about:
settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
break;
}
}
return settingTabs; return settingTabs;
} }
@ -198,26 +244,26 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
child: ListView( child: ListView(
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,
children: tabs children: tabs.map((tab) => _listItem(tab: tab)).toList(),
.asMap()
.entries
.map((tab) => _listItem(tab: tab.value, index: tab.key))
.toList(),
)); ));
} }
Widget _listItem({required _TabInfo tab, required int index}) { Widget _listItem({required _TabInfo tab}) {
return Obx(() { return Obx(() {
bool selected = index == selectedIndex.value; bool selected = tab.key == selectedTab.value;
return SizedBox( return SizedBox(
width: _kTabWidth, width: _kTabWidth,
height: _kTabHeight, height: _kTabHeight,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (selectedIndex.value != index) { if (selectedTab.value != tab.key) {
int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
if (index == -1) {
return;
}
controller.jumpToPage(index); controller.jumpToPage(index);
} }
selectedIndex.value = index; selectedTab.value = tab.key;
}, },
child: Row(children: [ child: Row(children: [
Container( Container(
@ -1117,7 +1163,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
child: Column(children: [ child: Column(children: [
server(enabled), server(enabled),
_Card(title: 'Proxy', children: [ _Card(title: 'Proxy', children: [
_Button('Socks5 Proxy', changeSocks5Proxy, _Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled), enabled: enabled),
]), ]),
]), ]),
@ -2033,7 +2079,12 @@ void changeSocks5Proxy() async {
password = pwdController.text.trim(); password = pwdController.text.trim();
if (proxy.isNotEmpty) { if (proxy.isNotEmpty) {
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy)); String domainPort = proxy;
if (domainPort.contains('://')) {
domainPort = domainPort.split('://')[1];
}
proxyMsg = translate(await bind.mainTestIfValidServer(
server: domainPort, testWithProxy: false));
if (proxyMsg.isEmpty) { if (proxyMsg.isEmpty) {
// ignore // ignore
} else { } else {
@ -2047,7 +2098,7 @@ void changeSocks5Proxy() async {
} }
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('Socks5 Proxy')), title: Text(translate('Socks5/Http(s) Proxy')),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
@ -2064,7 +2115,10 @@ void changeSocks5Proxy() async {
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null), errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
hintText: translate(
'Default protocol and port are Socks5 and 1080'),
),
controller: proxyController, controller: proxyController,
autofocus: true, autofocus: true,
), ),

View File

@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
@override @override
State<DesktopTabPage> createState() => _DesktopTabPageState(); State<DesktopTabPage> createState() => _DesktopTabPageState();
static void onAddSetting({int initialPage = 0}) { static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try { try {
DesktopTabController tabController = Get.find(); DesktopTabController tabController = Get.find();
tabController.add(TabInfo( tabController.add(TabInfo(
@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
unselectedIcon: Icons.build_outlined, unselectedIcon: Icons.build_outlined,
page: DesktopSettingPage( page: DesktopSettingPage(
key: const ValueKey(kTabLabelSettingPage), key: const ValueKey(kTabLabelSettingPage),
initialPage: initialPage, initialTabkey: initialPage,
))); )));
} catch (e) { } catch (e) {
debugPrintStack(label: '$e'); debugPrintStack(label: '$e');

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.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/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
@ -107,107 +108,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController.onRemoved = (_, id) => onRemoveId(id); tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
switchUuid: switchUuid,
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);
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 RemotePage).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 remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
}
_update_remote_count();
return returnValue;
});
if (!_isScreenRectSet) { if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@ -499,4 +400,130 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_update_remote_count() => _update_remote_count() =>
RemoteCountState.find().value = tabController.length; RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
switchUuid: switchUuid,
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);
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 RemotePage).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 remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.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 remotePage =
tabController.state.value.selectedTabInfo.page as RemotePage;
final ffi = remotePage.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());
}
}
}
_update_remote_count();
return returnValue;
}
} }

View File

@ -1045,7 +1045,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_screenAdjustor.updateScreen(); _screenAdjustor.updateScreen();
menuChildrenGetter() { menuChildrenGetter() {
final menuChildren = <Widget>[ final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context), _screenAdjustor.adjustWindow(context),
@ -1069,6 +1068,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi, ffi: widget.ffi,
), ),
Divider(), Divider(),
cursorToggles(),
Divider(),
toggles(), toggles(),
]; ];
// privacy mode // privacy mode
@ -1212,6 +1213,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
}); });
} }
cursorToggles() {
return futureBuilder(
future: toolbarCursor(context, id, ffi),
hasData: (data) {
final v = data as List<TToggleMenu>;
if (v.isEmpty) return Offstage();
return Column(
children: v
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList());
});
}
toggles() { toggles() {
return futureBuilder( return futureBuilder(
future: toolbarDisplayToggle(context, id, ffi), future: toolbarDisplayToggle(context, id, ffi),

View File

@ -836,6 +836,7 @@ void showOptions(
List<TRadioMenu<String>> imageQualityRadios = List<TRadioMenu<String>> imageQualityRadios =
await toolbarImageQuality(context, id, gFFI); await toolbarImageQuality(context, id, gFFI);
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI); List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
List<TToggleMenu> displayToggles = List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI); await toolbarDisplayToggle(context, id, gFFI);
@ -876,8 +877,23 @@ void showOptions(
})), })),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
]; ];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
final cursorTogglesList = cursorToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
},
title: e.value.child)))
.toList();
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
final toggles = displayToggles final displayTogglesList = displayToggles
.asMap() .asMap()
.entries .entries
.map((e) => Obx(() => CheckboxListTile( .map((e) => Obx(() => CheckboxListTile(
@ -890,6 +906,11 @@ void showOptions(
}, },
title: e.value.child))) title: e.value.child)))
.toList(); .toList();
final toggles = [
...cursorTogglesList,
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
...displayTogglesList,
];
Widget privacyModeWidget = Offstage(); Widget privacyModeWidget = Offstage();
if (privacyModeList.length > 1) { if (privacyModeList.length > 1) {

View File

@ -283,12 +283,3 @@ void setPrivacyModeDialog(
); );
}, backDismiss: true, clickMaskDismiss: true); }, backDismiss: true, clickMaskDismiss: true);
} }
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}

View File

@ -10,8 +10,8 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:http/http.dart' as http;
import '../utils/http_service.dart' as http;
import '../common.dart'; import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions'; final syncAbOption = 'sync-ab-with-recent-sessions';

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart'; import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -152,40 +153,36 @@ class TextureModel {
TextureModel(this.parent); TextureModel(this.parent);
setTextureType({required int display, required bool gpuTexture}) { setTextureType({required int display, required bool gpuTexture}) {
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture"); debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
var texture = _control[display]; ensureControl(display);
if (texture == null) { _control[display]?.setTextureType(gpuTexture: gpuTexture);
texture = _Control(); // For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
_control[display] = texture; final ffi = parent.target;
if (ffi == null) return;
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
if (currentDisplay != display) {
debugPrint(
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
ensureControl(currentDisplay);
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
}
} }
texture.setTextureType(gpuTexture: gpuTexture);
} }
setRgbaTextureId({required int display, required int id}) { setRgbaTextureId({required int display, required int id}) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { _control[display]?.setRgbaTextureId(id);
ctl = _Control();
_control[display] = ctl;
}
ctl.setRgbaTextureId(id);
} }
setGpuTextureId({required int display, required int id}) { setGpuTextureId({required int display, required int id}) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { _control[display]?.setGpuTextureId(id);
ctl = _Control();
_control[display] = ctl;
}
ctl.setGpuTextureId(id);
} }
RxInt getTextureId(int display) { RxInt getTextureId(int display) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { return _control[display]!.textureID;
ctl = _Control();
_control[display] = ctl;
}
return ctl.textureID;
} }
updateCurrentDisplay(int curDisplay) { updateCurrentDisplay(int curDisplay) {
@ -241,4 +238,12 @@ class TextureModel {
await texture.destroy(ffi); await texture.destroy(ffi);
} }
} }
ensureControl(int display) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
}
}
} }

View File

@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import '../utils/http_service.dart' as http;
class GroupModel { class GroupModel {
final RxBool groupLoading = false.obs; final RxBool groupLoading = false.obs;

View File

@ -4,9 +4,12 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../models/model.dart'; import '../../models/model.dart';
@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup'; const _kMouseEventUp = 'mouseup';
const _kMouseEventMove = 'mousemove'; const _kMouseEventMove = 'mousemove';
class CanvasCoords {
double x = 0;
double y = 0;
double scale = 1.0;
double scrollX = 0;
double scrollY = 0;
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
Size size = Size.zero;
CanvasCoords();
Map<String, dynamic> toJson() {
return {
'x': x,
'y': y,
'scale': scale,
'scrollX': scrollX,
'scrollY': scrollY,
'scrollStyle':
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
'size': {
'w': size.width,
'h': size.height,
}
};
}
static CanvasCoords fromJson(Map<String, dynamic> json) {
final model = CanvasCoords();
model.x = json['x'];
model.y = json['y'];
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
? ScrollStyle.scrollauto
: ScrollStyle.scrollbar;
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
static CanvasCoords fromCanvasModel(CanvasModel model) {
final coords = CanvasCoords();
coords.x = model.x;
coords.y = model.y;
coords.scale = model.scale;
coords.scrollX = model.scrollX;
coords.scrollY = model.scrollY;
coords.scrollStyle = model.scrollStyle;
coords.size = model.size;
return coords;
}
}
class CursorCoords {
Offset offset = Offset.zero;
CursorCoords();
Map<String, dynamic> toJson() {
return {
'offset_x': offset.dx,
'offset_y': offset.dy,
};
}
static CursorCoords fromJson(Map<String, dynamic> json) {
final model = CursorCoords();
model.offset = Offset(json['offset_x'], json['offset_y']);
return model;
}
static CursorCoords fromCursorModel(CursorModel model) {
final coords = CursorCoords();
coords.offset = model.offset;
return coords;
}
}
class RemoteWindowCoords {
RemoteWindowCoords(
this.windowRect, this.canvas, this.cursor, this.remoteRect);
Rect windowRect;
CanvasCoords canvas;
CursorCoords cursor;
Rect remoteRect;
Offset relativeOffset = Offset.zero;
Map<String, dynamic> toJson() {
return {
'canvas': canvas.toJson(),
'cursor': cursor.toJson(),
'windowRect': rectToJson(windowRect),
'remoteRect': rectToJson(remoteRect),
};
}
static Map<String, dynamic> rectToJson(Rect r) {
return {
'l': r.left,
't': r.top,
'w': r.width,
'h': r.height,
};
}
static Rect rectFromJson(Map<String, dynamic> json) {
return Rect.fromLTWH(
json['l'],
json['t'],
json['w'],
json['h'],
);
}
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
: windowRect = rectFromJson(json['windowRect']),
canvas = CanvasCoords.fromJson(json['canvas']),
cursor = CursorCoords.fromJson(json['cursor']),
remoteRect = rectFromJson(json['remoteRect']);
}
extension ToString on MouseButtons { extension ToString on MouseButtons {
String get value { String get value {
switch (this) { switch (this) {
@ -188,12 +313,17 @@ class InputModel {
int _lastButtons = 0; int _lastButtons = 0;
Offset lastMousePos = Offset.zero; Offset lastMousePos = Offset.zero;
bool _queryOtherWindowCoords = false;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
late final SessionID sessionId; late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? ''; String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly; bool get isViewOnly => parent.target!.ffiModel.viewOnly;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
@ -616,6 +746,9 @@ class InputModel {
void onPointDownImage(PointerDownEvent e) { void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}"); debugPrint("onPointDownImage ${e.kind}");
_stopFling = true; _stopFling = true;
if (isDesktop) _queryOtherWindowCoords = true;
_remoteWindowCoords = [];
_windowRect = null;
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@ -628,6 +761,7 @@ class InputModel {
} }
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@ -638,11 +772,37 @@ class InputModel {
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) {
Future.delayed(Duration.zero, () async {
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
_queryOtherWindowCoords = false;
}
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
List<RemoteWindowCoords> remoteWindowCoords) async {
final coords =
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
final wc = WindowController.fromWindowId(kWindowId!);
try {
final frame = await wc.getFrame();
for (final c in coords) {
c.relativeOffset = Offset(
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
remoteWindowCoords.add(c);
}
return frame;
} catch (e) {
// Unreachable code
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
}
return null;
}
void onPointerSignalImage(PointerSignalEvent e) { void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
@ -843,43 +1003,107 @@ class InputModel {
bool onExit = false, bool onExit = false,
int buttons = kPrimaryMouseButton, int buttons = kPrimaryMouseButton,
}) { }) {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel;
final ffiModel = parent.target!.ffiModel; final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
Rect? rect = ffiModel.rect;
if (isMove) { if (isMove) {
canvasModel.moveDesktopMouse(x, y); if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(x, y)) {
final coords =
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
isMove = false;
canvas = coords.canvas;
rect = coords.remoteRect;
x -= coords.relativeOffset.dx / devicePixelRatio;
y -= coords.relativeOffset.dy / devicePixelRatio;
}
}
} }
final nearThr = 3; y -= CanvasModel.topToEdge;
var nearRight = (canvasModel.size.width - x) < nearThr; x -= CanvasModel.leftToEdge;
var nearBottom = (canvasModel.size.height - y) < nearThr; if (isMove) {
final rect = ffiModel.rect; parent.target!.canvasModel.moveDesktopMouse(x, y);
}
return _handlePointerDevicePos(
kind,
x,
y,
isMove,
canvas,
rect,
evtType,
onExit: onExit,
buttons: buttons,
);
}
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
static RemoteWindowCoords? findRemoteCoords(double x, double y,
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
x *= devicePixelRatio;
y *= devicePixelRatio;
for (final c in remoteWindowCoords) {
if (x >= c.relativeOffset.dx &&
y >= c.relativeOffset.dy &&
x <= c.relativeOffset.dx + c.windowRect.width &&
y <= c.relativeOffset.dy + c.windowRect.height) {
return c;
}
}
return null;
}
Point? _handlePointerDevicePos(
String kind,
double x,
double y,
bool moveInCanvas,
CanvasCoords canvas,
Rect? rect,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
if (rect == null) { if (rect == null) {
return null; return null;
} }
final imageWidth = rect.width * canvasModel.scale;
final imageHeight = rect.height * canvasModel.scale; final nearThr = 3;
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { var nearRight = (canvas.size.width - x) < nearThr;
x += imageWidth * canvasModel.scrollX; var nearBottom = (canvas.size.height - y) < nearThr;
y += imageHeight * canvasModel.scrollY; final imageWidth = rect.width * canvas.scale;
final imageHeight = rect.height * canvas.scale;
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
x += imageWidth * canvas.scrollX;
y += imageHeight * canvas.scrollY;
// boxed size is a center widget // boxed size is a center widget
if (canvasModel.size.width > imageWidth) { if (canvas.size.width > imageWidth) {
x -= ((canvasModel.size.width - imageWidth) / 2); x -= ((canvas.size.width - imageWidth) / 2);
} }
if (canvasModel.size.height > imageHeight) { if (canvas.size.height > imageHeight) {
y -= ((canvasModel.size.height - imageHeight) / 2); y -= ((canvas.size.height - imageHeight) / 2);
} }
} else { } else {
x -= canvasModel.x; x -= canvas.x;
y -= canvasModel.y; y -= canvas.y;
} }
x /= canvasModel.scale; x /= canvas.scale;
y /= canvasModel.scale; y /= canvas.scale;
if (canvasModel.scale > 0 && canvasModel.scale < 1) { if (canvas.scale > 0 && canvas.scale < 1) {
final step = 1.0 / canvasModel.scale - 1; final step = 1.0 / canvas.scale - 1;
if (nearRight) { if (nearRight) {
x += step; x += step;
} }
@ -902,8 +1126,7 @@ class InputModel {
evtX = x.round(); evtX = x.round();
evtY = y.round(); evtY = y.round();
} catch (e) { } catch (e) {
debugPrintStack( debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return null; return null;
} }

View File

@ -50,6 +50,8 @@ class CachedPeerData {
Map<String, dynamic> peerInfo = {}; Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = []; List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {}; Map<String, dynamic> lastCursorId = {};
Map<String, bool> permissions = {};
bool secure = false; bool secure = false;
bool direct = false; bool direct = false;
@ -62,6 +64,7 @@ class CachedPeerData {
'peerInfo': peerInfo, 'peerInfo': peerInfo,
'cursorDataList': cursorDataList, 'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId, 'lastCursorId': lastCursorId,
'permissions': permissions,
'secure': secure, 'secure': secure,
'direct': direct, 'direct': direct,
}); });
@ -77,6 +80,9 @@ class CachedPeerData {
data.cursorDataList.add(cursorData); data.cursorDataList.add(cursorData);
} }
data.lastCursorId = map['lastCursorId']; data.lastCursorId = map['lastCursorId'];
map['permissions'].forEach((key, value) {
data.permissions[key] = value;
});
data.secure = map['secure']; data.secure = map['secure'];
data.direct = map['direct']; data.direct = map['direct'];
return data; return data;
@ -116,6 +122,10 @@ class FfiModel with ChangeNotifier {
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false; _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
Map<String, bool> get permissions => _permissions; Map<String, bool> get permissions => _permissions;
setPermissions(Map<String, bool> permissions) {
_permissions.clear();
_permissions.addAll(permissions);
}
bool? get secure => _secure; bool? get secure => _secure;
@ -138,6 +148,7 @@ class FfiModel with ChangeNotifier {
FfiModel(this.parent) { FfiModel(this.parent) {
clear(); clear();
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
} }
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true); Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@ -367,6 +378,8 @@ class FfiModel with ChangeNotifier {
} }
} else if (name == 'sync_peer_option') { } else if (name == 'sync_peer_option') {
_handleSyncPeerOption(evt, peerId); _handleSyncPeerOption(evt, peerId);
} else if (name == 'follow_current_display') {
handleFollowCurrentDisplay(evt, sessionId, peerId);
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Unknown event name: $name');
} }
@ -440,7 +453,7 @@ class FfiModel with ChangeNotifier {
} }
} }
updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) { updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
final newRect = displaysRect(); final newRect = displaysRect();
if (newRect == null) { if (newRect == null) {
return; return;
@ -1004,6 +1017,8 @@ class FfiModel with ChangeNotifier {
} }
} }
} }
parent.target!.canvasModel
.tryUpdateScrollStyle(Duration(milliseconds: 300), null);
notifyListeners(); notifyListeners();
} }
@ -1040,9 +1055,30 @@ class FfiModel with ChangeNotifier {
json.encode(_pi.platformAdditions); json.encode(_pi.platformAdditions);
} }
handleFollowCurrentDisplay(
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
if (evt['display_idx'] != null) {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
_pi.currentDisplay = int.parse(evt['display_idx']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([_pi.currentDisplay]),
);
}
notifyListeners();
}
// Directly switch to the new display without waiting for the response. // Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId, switchToNewDisplay(int display, SessionID sessionId, String peerId,
{bool updateCursorPos = true}) { {bool updateCursorPos = false}) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose(); parent.target?.recordingModel.onClose();
// no need to wait for the response // no need to wait for the response
@ -1378,10 +1414,20 @@ class CanvasModel with ChangeNotifier {
if (refreshMousePos) { if (refreshMousePos) {
parent.target?.inputModel.refreshMousePos(); parent.target?.inputModel.refreshMousePos();
} }
if (style == kRemoteViewStyleOriginal && tryUpdateScrollStyle(Duration.zero, style);
_scrollStyle == ScrollStyle.scrollbar) { }
updateScrollPercent();
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
if (style != kRemoteViewStyleOriginal) {
return;
} }
_resetScroll();
Future.delayed(duration, () async {
updateScrollPercent();
});
} }
updateScrollStyle() async { updateScrollStyle() async {
@ -1695,6 +1741,8 @@ class CursorModel with ChangeNotifier {
double _displayOriginX = 0; double _displayOriginX = 0;
double _displayOriginY = 0; double _displayOriginY = 0;
DateTime? _firstUpdateMouseTime; DateTime? _firstUpdateMouseTime;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
bool gotMouseControl = true; bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now() DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
@ -1707,6 +1755,8 @@ class CursorModel with ChangeNotifier {
double get x => _x - _displayOriginX; double get x => _x - _displayOriginX;
double get y => _y - _displayOriginY; double get y => _y - _displayOriginY;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
Offset get offset => Offset(_x, _y); Offset get offset => Offset(_x, _y);
double get hotx => _hotx; double get hotx => _hotx;
@ -1776,15 +1826,13 @@ class CursorModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
updatePan(double dx, double dy, bool touchMode) { updatePan(Offset delta, Offset localPosition, bool touchMode) {
if (touchMode) { if (touchMode) {
final scale = parent.target?.canvasModel.scale ?? 1.0; _handleTouchMode(delta, localPosition);
_x += dx / scale;
_y += dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
notifyListeners();
return; return;
} }
double dx = delta.dx;
double dy = delta.dy;
if (parent.target?.imageModel.image == null) return; if (parent.target?.imageModel.image == null) return;
final scale = parent.target?.canvasModel.scale ?? 1.0; final scale = parent.target?.canvasModel.scale ?? 1.0;
dx /= scale; dx /= scale;
@ -1851,6 +1899,41 @@ class CursorModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
_handleTouchMode(Offset delta, Offset localPosition) {
bool isMoved = false;
if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(localPosition.dx, localPosition.dy)) {
final coords = InputModel.findRemoteCoords(localPosition.dx,
localPosition.dy, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
double x2 =
(localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) /
coords.canvas.scale;
double y2 =
(localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) /
coords.canvas.scale;
x2 += coords.cursor.offset.dx;
y2 += coords.cursor.offset.dy;
parent.target?.inputModel.moveMouse(x2, y2);
isMoved = true;
}
}
if (!isMoved) {
final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += delta.dx / scale;
_y += delta.dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
}
notifyListeners();
}
updateCursorData(Map<String, dynamic> evt) async { updateCursorData(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']); final id = int.parse(evt['id']);
final hotx = double.parse(evt['hotx']); final hotx = double.parse(evt['hotx']);
@ -1990,6 +2073,18 @@ class CursorModel with ChangeNotifier {
deleteCustomCursor(k); deleteCustomCursor(k);
} }
} }
trySetRemoteWindowCoords() {
Future.delayed(Duration.zero, () async {
_windowRect =
await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
}
clearRemoteWindowCoords() {
_windowRect = null;
_remoteWindowCoords.clear();
}
} }
class QualityMonitorData { class QualityMonitorData {
@ -2319,6 +2414,7 @@ class FFI {
debugPrint('Unreachable, the cached data cannot be decoded.'); debugPrint('Unreachable, the cached data cannot be decoded.');
return; return;
} }
ffiModel.setPermissions(data.permissions);
await ffiModel.handleCachedPeerData(data, id); await ffiModel.handleCachedPeerData(data, id);
await sessionRefreshVideo(sessionId, ffiModel.pi); await sessionRefreshVideo(sessionId, ffiModel.pi);
await bind.sessionRequestNewDisplayInitMsgs( await bind.sessionRequestNewDisplayInitMsgs(

View File

@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../common.dart'; import '../common.dart';
import '../utils/http_service.dart' as http;
import 'model.dart'; import 'model.dart';
import 'platform_model.dart'; import 'platform_model.dart';
@ -136,7 +136,6 @@ class UserModel {
Future<LoginResponse> login(LoginRequest loginRequest) async { Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer(); final url = await bind.mainGetApiServer();
final resp = await http.post(Uri.parse('$url/api/login'), final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(loginRequest.toJson())); body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body; final Map<String, dynamic> body;

View File

@ -0,0 +1,115 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/platform_model.dart';
export 'package:http/http.dart' show Response;
enum HttpMethod { get, post, put, delete }
class HttpService {
Future<http.Response> sendRequest(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
headers ??= {'Content-Type': 'application/json'};
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
final isProxy = await bind.mainGetProxyStatus();
if (!isProxy) {
return await _pollFultterHttp(url, method, headers: headers, body: body);
}
String headersJson = jsonEncode(headers);
String methodName = method.toString().split('.').last;
await bind.mainHttpRequest(
url: url.toString(),
method: methodName.toLowerCase(),
body: body,
header: headersJson);
var resJson = await _pollForResponse(url.toString());
return _parseHttpResponse(resJson);
}
Future<http.Response> _pollFultterHttp(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
var response = http.Response('', 400);
switch (method) {
case HttpMethod.get:
response = await http.get(url, headers: headers);
break;
case HttpMethod.post:
response = await http.post(url, headers: headers, body: body);
break;
case HttpMethod.put:
response = await http.put(url, headers: headers, body: body);
break;
case HttpMethod.delete:
response = await http.delete(url, headers: headers, body: body);
break;
default:
throw Exception('Unsupported HTTP method');
}
return response;
}
Future<String> _pollForResponse(String url) async {
String? responseJson = " ";
while (responseJson == " ") {
responseJson = await bind.mainGetHttpStatus(url: url);
if (responseJson == null) {
throw Exception('The HTTP request failed');
}
if (responseJson == " ") {
await Future.delayed(const Duration(milliseconds: 100));
}
}
return responseJson!;
}
http.Response _parseHttpResponse(String responseJson) {
try {
var parsedJson = jsonDecode(responseJson);
String body = parsedJson['body'];
Map<String, String> headers = {};
for (var key in parsedJson['headers'].keys) {
headers[key] = parsedJson['headers'][key];
}
int statusCode = parsedJson['status_code'];
return http.Response(body, statusCode, headers: headers);
} catch (e) {
throw Exception('Failed to parse response: $e');
}
}
}
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
}
Future<http.Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
}
Future<http.Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
}
Future<http.Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
}

View File

@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order /// must keep the order
// ignore: constant_identifier_names // ignore: constant_identifier_names
@ -431,6 +433,39 @@ class RustDeskMultiWindowManager {
void unregisterActiveWindowListener(AsyncCallback callback) { void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback); _windowActiveCallbacks.remove(callback);
} }
// This function is called from the main window.
// It will query the active remote windows to get their coords.
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
List<String> coords = [];
for (final windowId in _remoteDesktopWindows) {
if (windowId != wId) {
if (_activeWindows.contains(windowId)) {
final res = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventRemoteWindowCoords, '');
if (res != null) {
coords.add(res);
}
}
}
}
return coords;
}
// This function is called from one remote window.
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
// So we need to call the main window to get the other remote windows' coords.
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
List<RemoteWindowCoords> coords = [];
// Call the main window to get the coords of other remote windows.
String res = await DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
List<dynamic> list = jsonDecode(res);
for (var item in list) {
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
}
return coords;
}
} }
final rustDeskWinManager = RustDeskMultiWindowManager.instance; final rustDeskWinManager = RustDeskMultiWindowManager.instance;

View File

@ -346,6 +346,10 @@ class RustdeskImpl {
return mode == kKeyLegacyMode; return mode == kKeyLegacyMode;
} }
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
return false;
}
Future<void> sessionSetCustomImageQuality( Future<void> sessionSetCustomImageQuality(
{required UuidValue sessionId, required int value, dynamic hint}) { {required UuidValue sessionId, required int value, dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [ return Future(() => js.context.callMethod('setByName', [
@ -676,7 +680,8 @@ class RustdeskImpl {
return Future(() => js.context.callMethod('setByName', ['options', json])); return Future(() => js.context.callMethod('setByName', ['options', json]));
} }
Future<String> mainTestIfValidServer({required String server, dynamic hint}) { Future<String> mainTestIfValidServer(
{required String server, required bool testWithProxy, dynamic hint}) {
// TODO: implement // TODO: implement
return Future.value(''); return Future.value('');
} }
@ -770,6 +775,24 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
Future<bool> mainGetProxyStatus({dynamic hint}) {
return Future(() => false);
}
Future<void> mainHttpRequest({
required String url,
required String method,
String? body,
required String header,
dynamic hint,
}) {
throw UnimplementedError();
}
Future<String?> mainGetHttpStatus({required String url, dynamic hint}) {
throw UnimplementedError();
}
String mainGetLocalOption({required String key, dynamic hint}) { String mainGetLocalOption({required String key, dynamic hint}) {
return js.context.callMethod('getByName', ['option:local', key]); return js.context.callMethod('getByName', ['option:local', key]);
} }

View File

@ -41,10 +41,19 @@ uuid = { version = "1.3", features = ["v4"] }
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 # crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
thiserror = "1.0" thiserror = "1.0"
httparse = "1.5"
base64 = "0.22"
url = "2.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1" mac_address = "1.1"
machine-uid = { git = "https://github.com/21pages/machine-uid" } machine-uid = { git = "https://github.com/21pages/machine-uid" }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
rustls-platform-verifier = "0.3.1"
rustls-pki-types = "1.4"
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
tokio-native-tls ="0.3"
[features] [features]
quic = [] quic = []

View File

@ -603,6 +603,8 @@ message OptionMessage {
// Resolution custom_resolution = 13; // Resolution custom_resolution = 13;
// BoolOption support_windows_specific_session = 14; // BoolOption support_windows_specific_session = 14;
// starting from 15 please, do not use removed fields // starting from 15 please, do not use removed fields
BoolOption follow_remote_cursor = 15;
BoolOption follow_remote_window = 16;
} }
message TestDelay { message TestDelay {
@ -765,6 +767,7 @@ message Misc {
uint32 selected_sid = 35; uint32 selected_sid = 35;
DisplayResolution change_display_resolution = 36; DisplayResolution change_display_resolution = 36;
MessageQuery message_query = 37; MessageQuery message_query = 37;
int32 follow_current_display = 38;
} }
} }

View File

@ -281,6 +281,10 @@ pub struct PeerConfig {
pub enable_file_transfer: EnableFileTransfer, pub enable_file_transfer: EnableFileTransfer,
#[serde(flatten)] #[serde(flatten)]
pub show_quality_monitor: ShowQualityMonitor, pub show_quality_monitor: ShowQualityMonitor,
#[serde(flatten)]
pub follow_remote_cursor: FollowRemoteCursor,
#[serde(flatten)]
pub follow_remote_window: FollowRemoteWindow,
#[serde( #[serde(
default, default,
deserialize_with = "deserialize_string", deserialize_with = "deserialize_string",
@ -353,6 +357,8 @@ impl Default for PeerConfig {
disable_clipboard: Default::default(), disable_clipboard: Default::default(),
enable_file_transfer: Default::default(), enable_file_transfer: Default::default(),
show_quality_monitor: Default::default(), show_quality_monitor: Default::default(),
follow_remote_cursor: Default::default(),
follow_remote_window: Default::default(),
keyboard_mode: Default::default(), keyboard_mode: Default::default(),
view_only: Default::default(), view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
@ -1258,6 +1264,19 @@ serde_field_bool!(
default_show_remote_cursor, default_show_remote_cursor,
"ShowRemoteCursor::default_show_remote_cursor" "ShowRemoteCursor::default_show_remote_cursor"
); );
serde_field_bool!(
FollowRemoteCursor,
"follow_remote_cursor",
default_follow_remote_cursor,
"FollowRemoteCursor::default_follow_remote_cursor"
);
serde_field_bool!(
FollowRemoteWindow,
"follow_remote_window",
default_follow_remote_window,
"FollowRemoteWindow::default_follow_remote_window"
);
serde_field_bool!( serde_field_bool!(
ShowQualityMonitor, ShowQualityMonitor,
"show_quality_monitor", "show_quality_monitor",

View File

@ -16,6 +16,7 @@ use std::{
}; };
pub use tokio; pub use tokio;
pub use tokio_util; pub use tokio_util;
pub mod proxy;
pub mod socket_client; pub mod socket_client;
pub mod tcp; pub mod tcp;
pub mod udp; pub mod udp;
@ -51,6 +52,7 @@ pub use serde_json;
pub use sysinfo; pub use sysinfo;
pub use toml; pub use toml;
pub use uuid; pub use uuid;
pub use base64;
pub use thiserror; pub use thiserror;
#[cfg(feature = "quic")] #[cfg(feature = "quic")]

View File

@ -0,0 +1,561 @@
use std::{
io::Error as IoError,
net::{SocketAddr, ToSocketAddrs},
};
use base64::{engine::general_purpose, Engine};
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
use log::info;
use thiserror::Error as ThisError;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
use tokio_rustls::{client::TlsStream, TlsConnector};
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr};
use tokio_util::codec::Framed;
use url::Url;
use crate::{
bytes_codec::BytesCodec,
config::Socks5Server,
tcp::{DynTcpStream, FramedStream},
ResultType,
};
#[derive(Debug, ThisError)]
pub enum ProxyError {
#[error("IO Error: {0}")]
IoError(#[from] IoError),
#[error("Target parse error: {0}")]
TargetParseError(String),
#[error("HTTP parse error: {0}")]
HttpParseError(#[from] HttpParseError),
#[error("The maximum response header length is exceeded: {0}")]
MaximumResponseHeaderLengthExceeded(usize),
#[error("The end of file is reached")]
EndOfFile,
#[error("The url is error: {0}")]
UrlBadScheme(String),
#[error("The url parse error: {0}")]
UrlParseScheme(#[from] url::ParseError),
#[error("No HTTP code was found in the response")]
NoHttpCode,
#[error("The HTTP code is not equal 200: {0}")]
HttpCode200(u16),
#[error("The proxy address resolution failed: {0}")]
AddressResolutionFailed(String),
#[cfg(any(target_os = "windows", target_os = "macos"))]
#[error("The native tls error: {0}")]
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
}
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
/// The maximum HTTP Headers, which can be parsed.
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
const DEFINE_TIME_OUT: u64 = 600;
pub trait IntoUrl {
// Besides parsing as a valid `Url`, the `Url` must be a valid
// `http::Uri`, in that it makes sense to use in a network request.
fn into_url(self) -> Result<Url, ProxyError>;
fn as_str(&self) -> &str;
}
impl IntoUrl for Url {
fn into_url(self) -> Result<Url, ProxyError> {
if self.has_host() {
Ok(self)
} else {
Err(ProxyError::UrlBadScheme(self.to_string()))
}
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
impl<'a> IntoUrl for &'a str {
fn into_url(self) -> Result<Url, ProxyError> {
Url::parse(self)
.map_err(ProxyError::UrlParseScheme)?
.into_url()
}
fn as_str(&self) -> &str {
self
}
}
impl<'a> IntoUrl for &'a String {
fn into_url(self) -> Result<Url, ProxyError> {
(&**self).into_url()
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
impl<'a> IntoUrl for String {
fn into_url(self) -> Result<Url, ProxyError> {
(&*self).into_url()
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone)]
pub struct Auth {
user_name: String,
password: String,
}
impl Auth {
fn get_proxy_authorization(&self) -> String {
format!(
"Proxy-Authorization: Basic {}\r\n",
self.get_basic_authorization()
)
}
pub fn get_basic_authorization(&self) -> String {
let authorization = format!("{}:{}", &self.user_name, &self.password);
general_purpose::STANDARD.encode(authorization.as_bytes())
}
}
#[derive(Clone)]
pub enum ProxyScheme {
Http {
auth: Option<Auth>,
host: String,
},
Https {
auth: Option<Auth>,
host: String,
},
Socks5 {
addr: SocketAddr,
auth: Option<Auth>,
remote_dns: bool,
},
}
impl ProxyScheme {
pub fn maybe_auth(&self) -> Option<&Auth> {
match self {
ProxyScheme::Http { auth, .. }
| ProxyScheme::Https { auth, .. }
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
}
}
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Socks5 {
addr,
auth: None,
remote_dns: false,
})
}
fn http(host: &str) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Http {
auth: None,
host: host.to_string(),
})
}
fn https(host: &str) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Https {
auth: None,
host: host.to_string(),
})
}
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
let auth = Auth {
user_name: username.into(),
password: password.into(),
};
match self {
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
}
}
fn parse(url: Url) -> Result<Self, ProxyError> {
use url::Position;
// Resolve URL to a host and port
let to_addr = || {
let addrs = url.socket_addrs(|| match url.scheme() {
"socks5" => Some(1080),
_ => None,
})?;
addrs
.into_iter()
.next()
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
};
let mut scheme: Self = match url.scheme() {
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
"socks5" => Self::socks5(to_addr()?)?,
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
};
if let Some(pwd) = url.password() {
let username = url.username();
scheme.set_basic_auth(username, pwd);
}
Ok(scheme)
}
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
info!("Resolving socket address");
match self {
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
}
}
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
let (host_str, port) = match host.split_once(':') {
Some((h, p)) => (h, p.parse::<u16>().ok()),
None => (host, None),
};
let addr = (host_str, port.unwrap_or(default_port))
.to_socket_addrs()?
.next()
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
Ok(addr)
}
pub fn get_domain(&self) -> Result<String, ProxyError> {
match self {
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
let domain = host
.split(':')
.next()
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
Ok(domain.to_string())
}
ProxyScheme::Socks5 { addr, .. } => match addr {
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
},
}
}
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
match self {
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
}
}
fn append_default_port(&self, host: &str, default_port: u16) -> String {
if host.contains(':') {
host.to_string()
} else {
format!("{}:{}", host, default_port)
}
}
}
pub trait IntoProxyScheme {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
}
impl<S: IntoUrl> IntoProxyScheme for S {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
// validate the URL
let url = match self.as_str().into_url() {
Ok(ok) => ok,
Err(e) => {
match e {
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
ProxyError::UrlParseScheme(_source) => {
let try_this = format!("socks5://{}", self.as_str());
try_this.into_url()?
}
_ => {
return Err(e);
}
}
}
};
ProxyScheme::parse(url)
}
}
impl IntoProxyScheme for ProxyScheme {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
Ok(self)
}
}
#[derive(Clone)]
pub struct Proxy {
pub intercept: ProxyScheme,
ms_timeout: u64,
}
impl Proxy {
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
Ok(Self {
intercept: proxy_scheme.into_proxy_scheme()?,
ms_timeout,
})
}
pub fn is_http_or_https(&self) -> bool {
return match self.intercept {
ProxyScheme::Socks5 { .. } => false,
_ => true,
};
}
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
let mut proxy;
match ms_timeout {
None => {
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
}
Some(time_out) => {
proxy = Self::new(&conf.proxy, time_out)?;
}
}
if !conf.password.is_empty() && !conf.username.is_empty() {
proxy = proxy.basic_auth(&conf.username, &conf.password);
}
Ok(proxy)
}
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
self.intercept.socket_addrs().await
}
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
self.intercept.set_basic_auth(username, password);
self
}
pub async fn connect<'t, T>(
self,
target: T,
local_addr: Option<SocketAddr>,
) -> ResultType<FramedStream>
where
T: IntoTargetAddr<'t>,
{
info!("Connect to proxy server");
let proxy = self.proxy_addrs().await?;
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
let stream = super::timeout(
self.ms_timeout,
crate::tcp::new_socket(local, true)?.connect(proxy),
)
.await??;
stream.set_nodelay(true).ok();
let addr = stream.local_addr()?;
return match self.intercept {
ProxyScheme::Http { .. } => {
info!("Connect to remote http proxy server: {}", proxy);
let stream =
super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??;
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
ProxyScheme::Https { .. } => {
info!("Connect to remote https proxy server: {}", proxy);
let stream =
super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??;
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
ProxyScheme::Socks5 { .. } => {
info!("Connect to remote socket5 proxy server: {}", proxy);
let stream = if let Some(auth) = self.intercept.maybe_auth() {
super::timeout(
self.ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream,
target,
&auth.user_name,
&auth.password,
),
)
.await??
} else {
super::timeout(
self.ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
};
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
};
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub async fn https_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
let stream = tls_connector
.connect(&self.intercept.get_domain()?, io)
.await?;
self.http_connect(stream, target).await
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub async fn https_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
use std::convert::TryFrom;
let verifier = rustls_platform_verifier::tls_config();
let url_domain = self.intercept.get_domain()?;
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
.to_owned();
let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier));
let stream = tls_connector.connect(domain, io).await?;
self.http_connect(stream, target).await
}
pub async fn http_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<Input>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
let mut stream = BufStream::new(io);
let (domain, port) = get_domain_and_port(target)?;
let request = self.make_request(&domain, port);
stream.write_all(request.as_bytes()).await?;
stream.flush().await?;
recv_and_check_response(&mut stream).await?;
Ok(stream)
}
fn make_request(&self, host: &str, port: u16) -> String {
let mut request = format!(
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
host = host,
port = port
);
if let Some(auth) = self.intercept.maybe_auth() {
request = format!("{}{}", request, auth.get_proxy_authorization());
}
request.push_str("\r\n");
request
}
}
fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> {
let target_addr = target
.into_target_addr()
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
match target_addr {
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)),
}
}
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
use tokio::io::AsyncBufReadExt;
let mut response = String::new();
loop {
if stream.read_line(&mut response).await? == 0 {
return Err(ProxyError::EndOfFile);
}
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
response.len(),
));
}
if response.ends_with("\r\n\r\n") {
return Ok(response);
}
}
}
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
let response_string = get_response(stream).await?;
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
let mut response = Response::new(&mut response_headers);
let response_bytes = response_string.into_bytes();
response.parse(&response_bytes)?;
return match response.code {
Some(code) => {
if code == 200 {
Ok(())
} else {
Err(ProxyError::HttpCode200(code))
}
}
None => Err(ProxyError::NoHttpCode),
};
}

View File

@ -49,19 +49,27 @@ pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
host host
} }
pub fn test_if_valid_server(host: &str) -> String { pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
let host = check_port(host, 0); let host = check_port(host, 0);
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
match Config::get_network_type() {
NetworkType::Direct => match host.to_socket_addrs() { if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
test_if_valid_server_for_proxy_(&host)
} else {
match host.to_socket_addrs() {
Err(err) => err.to_string(), Err(err) => err.to_string(),
Ok(_) => "".to_owned(), Ok(_) => "".to_owned(),
}, }
NetworkType::ProxySocks => match &host.into_target_addr() { }
Err(err) => err.to_string(), }
Ok(_) => "".to_owned(),
}, #[inline]
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
// it can be used for both `socks` and `http` proxy.
match &host.into_target_addr() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
} }
} }
@ -107,15 +115,7 @@ pub async fn connect_tcp_local<
ms_timeout: u64, ms_timeout: u64,
) -> ResultType<FramedStream> { ) -> ResultType<FramedStream> {
if let Some(conf) = Config::get_socks() { if let Some(conf) = Config::get_socks() {
return FramedStream::connect( return FramedStream::connect(target, local, &conf, ms_timeout).await;
conf.proxy.as_str(),
target,
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
.await;
} }
if let Some(target) = target.resolve() { if let Some(target) = target.resolve() {
if let Some(local) = local { if let Some(local) = local {
@ -255,10 +255,20 @@ mod tests {
#[test] #[test]
fn test_test_if_valid_server() { fn test_test_if_valid_server() {
assert!(!test_if_valid_server("a").is_empty()); assert!(!test_if_valid_server("a", false).is_empty());
// on Linux, "1" is resolved to "0.0.0.1" // on Linux, "1" is resolved to "0.0.0.1"
assert!(test_if_valid_server("1.1.1.1").is_empty()); assert!(test_if_valid_server("1.1.1.1", false).is_empty());
assert!(test_if_valid_server("1.1.1.1:1").is_empty()); assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
assert!(test_if_valid_server("abcd.com", false).is_empty());
assert!(test_if_valid_server("abcd.com:1", false).is_empty());
// with proxy
// `:0` indicates `let host = check_port(host, 0);` is called.
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
} }
#[test] #[test]

View File

@ -1,4 +1,4 @@
use crate::{bail, bytes_codec::BytesCodec, ResultType}; use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
use anyhow::Context as AnyhowCtx; use anyhow::Context as AnyhowCtx;
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
@ -18,20 +18,20 @@ use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf}, io::{AsyncRead, AsyncWrite, ReadBuf},
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs}, net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
}; };
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs}; use tokio_socks::IntoTargetAddr;
use tokio_util::codec::Framed; use tokio_util::codec::Framed;
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {} pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>); pub struct DynTcpStream(pub(crate) Box<dyn TcpStreamTrait + Send + Sync>);
#[derive(Clone)] #[derive(Clone)]
pub struct Encrypt(Key, u64, u64); pub struct Encrypt(Key, u64, u64);
pub struct FramedStream( pub struct FramedStream(
Framed<DynTcpStream, BytesCodec>, pub(crate) Framed<DynTcpStream, BytesCodec>,
SocketAddr, pub(crate) SocketAddr,
Option<Encrypt>, pub(crate) Option<Encrypt>,
u64, pub(crate) u64,
); );
impl Deref for FramedStream { impl Deref for FramedStream {
@ -62,7 +62,7 @@ impl DerefMut for DynTcpStream {
} }
} }
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> { pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
let socket = match addr { let socket = match addr {
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?, std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?, std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
@ -109,51 +109,17 @@ impl FramedStream {
bail!(format!("Failed to connect to {remote_addr}")); bail!(format!("Failed to connect to {remote_addr}"));
} }
pub async fn connect<'a, 't, P, T>( pub async fn connect<'t, T>(
proxy: P,
target: T, target: T,
local_addr: Option<SocketAddr>, local_addr: Option<SocketAddr>,
username: &'a str, proxy_conf: &Socks5Server,
password: &'a str,
ms_timeout: u64, ms_timeout: u64,
) -> ResultType<Self> ) -> ResultType<Self>
where where
P: ToProxyAddrs,
T: IntoTargetAddr<'t>, T: IntoTargetAddr<'t>,
{ {
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await { let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
let local = if let Some(addr) = local_addr { proxy.connect::<T>(target, local_addr).await
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
let stream =
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
stream.set_nodelay(true).ok();
let stream = if username.trim().is_empty() {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
} else {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream, target, username, password,
),
)
.await??
};
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
bail!("could not resolve to any address");
} }
pub fn local_addr(&self) -> SocketAddr { pub fn local_addr(&self) -> SocketAddr {

View File

@ -221,7 +221,6 @@ impl Encoder {
let h265_useable = let h265_useable =
_all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some()); _all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some());
let mut name = ENCODE_CODEC_NAME.lock().unwrap(); let mut name = ENCODE_CODEC_NAME.lock().unwrap();
let mut preference = PreferCodec::Auto;
let preferences: Vec<_> = decodings let preferences: Vec<_> = decodings
.iter() .iter()
.filter(|(_, s)| { .filter(|(_, s)| {
@ -233,9 +232,20 @@ impl Encoder {
}) })
.map(|(_, s)| s.prefer) .map(|(_, s)| s.prefer)
.collect(); .collect();
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { // find the most frequent preference
preference = preferences[0].enum_value_or(PreferCodec::Auto); let mut counts = Vec::new();
for pref in &preferences {
match counts.iter_mut().find(|(p, _)| p == pref) {
Some((_, count)) => *count += 1,
None => counts.push((pref.clone(), 1)),
}
} }
let max_count = counts.iter().map(|(_, count)| *count).max().unwrap_or(0);
let (most_frequent, _) = counts
.into_iter()
.find(|(_, count)| *count == max_count)
.unwrap_or((PreferCodec::Auto.into(), 0));
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
#[allow(unused_mut)] #[allow(unused_mut)]
let mut auto_codec = CodecName::VP9; let mut auto_codec = CodecName::VP9;

View File

@ -17,15 +17,13 @@ use hbb_common::{
}; };
use hwcodec::{ use hwcodec::{
common::{DataFormat, Driver, MAX_GOP}, common::{DataFormat, Driver, MAX_GOP},
native::{ vram::{
decode::{self, DecodeFrame, Decoder}, decode::{self, DecodeFrame, Decoder},
encode::{self, EncodeFrame, Encoder}, encode::{self, EncodeFrame, Encoder},
Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext,
}, },
}; };
const OUTPUT_SHARED_HANDLE: bool = false;
// https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/ // https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/
// https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/ // https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/
// https://cybersided.com/two-monitors-two-gpus/ // https://cybersided.com/two-monitors-two-gpus/
@ -294,6 +292,10 @@ impl VRamDecoder {
pub fn try_get(format: CodecFormat, luid: Option<i64>) -> Option<DecodeContext> { pub fn try_get(format: CodecFormat, luid: Option<i64>) -> Option<DecodeContext> {
let v: Vec<_> = Self::available(format, luid); let v: Vec<_> = Self::available(format, luid);
if v.len() > 0 { if v.len() > 0 {
// prefer ffmpeg
if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) {
return Some(ctx.clone());
}
Some(v[0].clone()) Some(v[0].clone())
} else { } else {
None None
@ -327,8 +329,8 @@ impl VRamDecoder {
} }
pub fn new(format: CodecFormat, luid: Option<i64>) -> ResultType<Self> { pub fn new(format: CodecFormat, luid: Option<i64>) -> ResultType<Self> {
log::info!("try create {format:?} vram decoder, luid: {luid:?}");
let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?; let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?;
log::info!("try create vram decoder: {ctx:?}");
match Decoder::new(ctx) { match Decoder::new(ctx) {
Ok(decoder) => Ok(Self { decoder }), Ok(decoder) => Ok(Self { decoder }),
Err(_) => { Err(_) => {
@ -372,7 +374,7 @@ pub(crate) fn check_available_vram() -> String {
gop: MAX_GOP as _, gop: MAX_GOP as _,
}; };
let encoders = encode::available(d); let encoders = encode::available(d);
let decoders = decode::available(OUTPUT_SHARED_HANDLE); let decoders = decode::available();
let available = Available { let available = Available {
e: encoders, e: encoders,
d: decoders, d: decoders,

View File

@ -23,17 +23,21 @@ SECRET_KEY = os.getenv("SECRET_KEY") or "worldpeace2024"
# The headers for API requests # The headers for API requests
HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"} HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"}
TIMEOUT = int(os.getenv("TIMEOUT") or "30") SIGN_TIMEOUT = int(os.getenv("SIGN_TIMEOUT") or "30")
TIMEOUT = float(os.getenv("TIMEOUT") or "900")
def create(task_name, file_path=None): def create(task_name, file_path=None):
if file_path is None: if file_path is None:
response = requests.post(f"{BASE_URL}/tasks/{task_name}", headers=HEADERS) response = requests.post(
f"{BASE_URL}/tasks/{task_name}", timeout=TIMEOUT, headers=HEADERS
)
else: else:
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
files = {"file": f} files = {"file": f}
response = requests.post( response = requests.post(
f"{BASE_URL}/tasks/{task_name}", f"{BASE_URL}/tasks/{task_name}",
timeout=TIMEOUT,
headers=HEADERS, headers=HEADERS,
files=files, files=files,
) )
@ -44,19 +48,27 @@ def upload_file(task_id, file_path):
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
files = {"file": f} files = {"file": f}
response = requests.post( response = requests.post(
f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, files=files f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
files=files,
) )
return get_json(response) return get_json(response)
def get_status(task_id): def get_status(task_id):
response = requests.get(f"{BASE_URL}/tasks/{task_id}/status", headers=HEADERS) response = requests.get(
f"{BASE_URL}/tasks/{task_id}/status", timeout=TIMEOUT, headers=HEADERS
)
return get_json(response) return get_json(response)
def download_files(task_id, output_dir, fn=None): def download_files(task_id, output_dir, fn=None):
response = requests.get( response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, stream=True f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
) )
# Check if the request was successful # Check if the request was successful
@ -73,7 +85,10 @@ def download_files(task_id, output_dir, fn=None):
def download_one_file(task_id, file_id, output_dir): def download_one_file(task_id, file_id, output_dir):
response = requests.get( response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files/{file_id}", headers=HEADERS, stream=True f"{BASE_URL}/tasks/{task_id}/files/{file_id}",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
) )
# Check if the request was successful # Check if the request was successful
@ -86,14 +101,19 @@ def download_one_file(task_id, file_id, output_dir):
return response.ok return response.ok
def fetch(): def fetch(tag=None):
response = requests.get(f"{BASE_URL}/tasks/fetch_task", headers=HEADERS) response = requests.get(
f"{BASE_URL}/tasks/fetch_task" + ("?tag=%s" % tag if tag else ""),
timeout=TIMEOUT,
headers=HEADERS,
)
return get_json(response) return get_json(response)
def update_status(task_id, status): def update_status(task_id, status):
response = requests.patch( response = requests.patch(
f"{BASE_URL}/tasks/{task_id}/status", f"{BASE_URL}/tasks/{task_id}/status",
timeout=TIMEOUT,
headers=HEADERS, headers=HEADERS,
json=status, json=status,
) )
@ -103,6 +123,7 @@ def update_status(task_id, status):
def delete_task(task_id): def delete_task(task_id):
response = requests.delete( response = requests.delete(
f"{BASE_URL}/tasks/{task_id}", f"{BASE_URL}/tasks/{task_id}",
timeout=TIMEOUT,
headers=HEADERS, headers=HEADERS,
) )
return get_json(response) return get_json(response)
@ -135,7 +156,7 @@ def sign_one_file(file_path):
task_id = res["id"] task_id = res["id"]
n = 0 n = 0
while True: while True:
if n >= TIMEOUT: if n >= SIGN_TIMEOUT:
delete_task(task_id) delete_task(task_id)
logging.error(f"Failed to sign {file_path}") logging.error(f"Failed to sign {file_path}")
break break

2
res/msi/.gitignore vendored
View File

@ -9,3 +9,5 @@ packages
CustomActions/x64 CustomActions/x64
CustomActions/*.user CustomActions/*.user
CustomActions/*.filters CustomActions/*.filters
Package/Resources

View File

@ -16,7 +16,7 @@
<Icon Id="AppIcon" SourceFile="Resources\icon.ico" /> <Icon Id="AppIcon" SourceFile="Resources\icon.ico" />
<!-- User Interface --> <!-- User Interface -->
<WixVariable Id="WixUILicenseRtf" Value="RustDesk License.rtf" /> <WixVariable Id="WixUILicenseRtf" Value="License.rtf" />
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" /> <ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<UIRef Id="WixUI_ErrorProgressText" /> <UIRef Id="WixUI_ErrorProgressText" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@ -9,6 +9,7 @@ import datetime
import subprocess import subprocess
import re import re
from pathlib import Path from pathlib import Path
import shutil
g_indent_unit = "\t" g_indent_unit = "\t"
g_version = "" g_version = ""
@ -391,6 +392,19 @@ def gen_content_between_tags(filename, tag_start, tag_end, func):
return True return True
def prepare_resources():
icon_src = Path(sys.argv[0]).parent.joinpath("../icon.ico")
icon_dst = Path(sys.argv[0]).parent.joinpath("Package/Resources/icon.ico")
if icon_src.exists():
icon_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(icon_src, icon_dst)
return True
else:
# unreachable
print(f"Error: icon.ico not found in {icon_src}")
return False
def init_global_vars(dist_dir, app_name, args): def init_global_vars(dist_dir, app_name, args):
dist_app = dist_dir.joinpath(app_name + ".exe") dist_app = dist_dir.joinpath(app_name + ".exe")
@ -423,6 +437,19 @@ def init_global_vars(dist_dir, app_name, args):
return True return True
def update_license_file(app_name):
if app_name == "RustDesk":
return
license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf")
with open(license_file, "r") as f:
license_content = f.read()
license_content = license_content.replace("website rustdesk.com and other ", "")
license_content = license_content.replace("RustDesk", app_name)
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
with open(license_file, "w") as f:
f.write(license_content)
def replace_component_guids_in_wxs(): def replace_component_guids_in_wxs():
langs_dir = Path(sys.argv[0]).parent.joinpath("Package") langs_dir = Path(sys.argv[0]).parent.joinpath("Package")
for file_path in langs_dir.glob("**/*.wxs"): for file_path in langs_dir.glob("**/*.wxs"):
@ -446,9 +473,14 @@ if __name__ == "__main__":
app_name = args.app_name app_name = args.app_name
dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve() dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve()
if not prepare_resources():
sys.exit(-1)
if not init_global_vars(dist_dir, app_name, args): if not init_global_vars(dist_dir, app_name, args):
sys.exit(-1) sys.exit(-1)
update_license_file(app_name)
if not gen_pre_vars(args, dist_dir): if not gen_pre_vars(args, dist_dir):
sys.exit(-1) sys.exit(-1)

View File

@ -1456,6 +1456,22 @@ impl LoginConfigHandler {
BoolOption::No BoolOption::No
}) })
.into(); .into();
} else if name == "follow-remote-cursor" {
config.follow_remote_cursor.v = !config.follow_remote_cursor.v;
option.follow_remote_cursor = (if config.follow_remote_cursor.v {
BoolOption::Yes
} else {
BoolOption::No
})
.into();
} else if name == "follow-remote-window" {
config.follow_remote_window.v = !config.follow_remote_window.v;
option.follow_remote_window = (if config.follow_remote_window.v {
BoolOption::Yes
} else {
BoolOption::No
})
.into();
} else if name == "disable-audio" { } else if name == "disable-audio" {
config.disable_audio.v = !config.disable_audio.v; config.disable_audio.v = !config.disable_audio.v;
option.disable_audio = (if config.disable_audio.v { option.disable_audio = (if config.disable_audio.v {
@ -1564,7 +1580,10 @@ impl LoginConfigHandler {
/// ///
/// * `ignore_default` - If `true`, ignore the default value of the option. /// * `ignore_default` - If `true`, ignore the default value of the option.
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> { fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
if self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP) || self.conn_type.eq(&ConnType::FILE_TRANSFER) { if self.conn_type.eq(&ConnType::PORT_FORWARD)
|| self.conn_type.eq(&ConnType::RDP)
|| self.conn_type.eq(&ConnType::FILE_TRANSFER)
{
return None; return None;
} }
let mut msg = OptionMessage::new(); let mut msg = OptionMessage::new();
@ -1601,6 +1620,12 @@ impl LoginConfigHandler {
if view_only || self.get_toggle_option("show-remote-cursor") { if view_only || self.get_toggle_option("show-remote-cursor") {
msg.show_remote_cursor = BoolOption::Yes.into(); msg.show_remote_cursor = BoolOption::Yes.into();
} }
if self.get_toggle_option("follow-remote-cursor") {
msg.follow_remote_cursor = BoolOption::Yes.into();
}
if self.get_toggle_option("follow-remote-window") {
msg.follow_remote_window = BoolOption::Yes.into();
}
if !view_only && self.get_toggle_option("lock-after-session-end") { if !view_only && self.get_toggle_option("lock-after-session-end") {
msg.lock_after_session_end = BoolOption::Yes.into(); msg.lock_after_session_end = BoolOption::Yes.into();
} }
@ -1692,6 +1717,10 @@ impl LoginConfigHandler {
self.config.allow_swap_key.v self.config.allow_swap_key.v
} else if name == "view-only" { } else if name == "view-only" {
self.config.view_only.v self.config.view_only.v
} else if name == "follow-remote-cursor" {
self.config.follow_remote_cursor.v
} else if name == "follow-remote-window" {
self.config.follow_remote_window.v
} else { } else {
!self.get_option(name).is_empty() !self.get_option(name).is_empty()
} }
@ -2084,7 +2113,7 @@ where
std::thread::spawn(move || { std::thread::spawn(move || {
#[cfg(windows)] #[cfg(windows)]
sync_cpu_usage(); sync_cpu_usage();
let mut handler_controller_map = Vec::new(); let mut handler_controller_map = HashMap::new();
// let mut count = Vec::new(); // let mut count = Vec::new();
// let mut duration = std::time::Duration::ZERO; // let mut duration = std::time::Duration::ZERO;
// let mut skip_beginning = Vec::new(); // let mut skip_beginning = Vec::new();
@ -2115,17 +2144,18 @@ where
let display = vf.display as usize; let display = vf.display as usize;
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let format = CodecFormat::from(&vf); let format = CodecFormat::from(&vf);
if handler_controller_map.len() <= display { if !handler_controller_map.contains_key(&display) {
for _i in handler_controller_map.len()..=display { handler_controller_map.insert(
handler_controller_map.push(VideoHandlerController { display,
handler: VideoHandler::new(format, _i), VideoHandlerController {
handler: VideoHandler::new(format, display),
count: 0, count: 0,
duration: std::time::Duration::ZERO, duration: std::time::Duration::ZERO,
skip_beginning: 0, skip_beginning: 0,
}); },
} );
} }
if let Some(handler_controller) = handler_controller_map.get_mut(display) { if let Some(handler_controller) = handler_controller_map.get_mut(&display) {
let mut pixelbuffer = true; let mut pixelbuffer = true;
let mut tmp_chroma = None; let mut tmp_chroma = None;
match handler_controller.handler.handle_frame( match handler_controller.handler.handle_frame(
@ -2193,7 +2223,7 @@ where
let mut should_update_supported = false; let mut should_update_supported = false;
handler_controller_map handler_controller_map
.iter() .iter()
.map(|h| { .map(|(_, h)| {
if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER { if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER {
let mut lc = session.lc.write().unwrap(); let mut lc = session.lc.write().unwrap();
let format = h.handler.decoder.format(); let format = h.handler.decoder.format();
@ -2212,21 +2242,20 @@ where
} }
} }
MediaData::Reset(display) => { MediaData::Reset(display) => {
if let Some(handler_controler) = handler_controller_map.get_mut(display) { if let Some(handler_controler) = handler_controller_map.get_mut(&display) {
handler_controler.handler.reset(None); handler_controler.handler.reset(None);
} }
} }
MediaData::RecordScreen(start, display, w, h, id) => { MediaData::RecordScreen(start, display, w, h, id) => {
log::info!("record screen command: start: {start}, display: {display}"); log::info!("record screen command: start: {start}, display: {display}");
if handler_controller_map.len() == 1 { // Compatible with the sciter version(single ui session).
// Compatible with the sciter version(single ui session). // For the sciter version, there're no multi-ui-sessions for one connection.
// For the sciter version, there're no multi-ui-sessions for one connection. // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler.
// The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. if let Some(handler_controler) = handler_controller_map.get_mut(&display) {
handler_controller_map[0] handler_controler.handler.record_screen(start, w, h, id);
.handler } else if handler_controller_map.len() == 1 {
.record_screen(start, w, h, id); if let Some(handler_controler) =
} else { handler_controller_map.values_mut().next()
if let Some(handler_controler) = handler_controller_map.get_mut(display)
{ {
handler_controler.handler.record_screen(start, w, h, id); handler_controler.handler.record_screen(start, w, h, id);
} }

View File

@ -1504,7 +1504,9 @@ impl<T: InvokeUiSession> Remote<T> {
log::info!("update supported encoding:{:?}", e); log::info!("update supported encoding:{:?}", e);
self.handler.lc.write().unwrap().supported_encoding = e; self.handler.lc.write().unwrap().supported_encoding = e;
} }
Some(misc::Union::FollowCurrentDisplay(d_idx)) => {
self.handler.set_current_display(d_idx);
}
_ => {} _ => {}
}, },
Some(message::Union::TestDelay(t)) => { Some(message::Union::TestDelay(t)) => {

View File

@ -5,6 +5,8 @@ use std::{
task::Poll, task::Poll,
}; };
use serde_json::Value;
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum GrabState { pub enum GrabState {
Ready, Ready,
@ -123,7 +125,7 @@ use hbb_common::compress::decompress;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
anyhow::{anyhow, Context}, anyhow::{anyhow, Context},
bail, bail, base64,
bytes::Bytes, bytes::Bytes,
compress::compress as compress_func, compress::compress as compress_func,
config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT}, config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT},
@ -145,7 +147,10 @@ use hbb_common::{
// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all};
use crate::ui_interface::{get_option, set_option}; use crate::{
hbbs_http::create_http_client_async,
ui_interface::{get_option, set_option},
};
pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future<Output = ()>; pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future<Output = ()>;
@ -972,7 +977,7 @@ pub fn check_software_update() {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn check_software_update_() -> hbb_common::ResultType<()> { async fn check_software_update_() -> hbb_common::ResultType<()> {
let url = "https://github.com/rustdesk/rustdesk/releases/latest"; let url = "https://github.com/rustdesk/rustdesk/releases/latest";
let latest_release_response = reqwest::get(url).await?; let latest_release_response = create_http_client_async().get(url).send().await?;
let latest_release_version = latest_release_response let latest_release_version = latest_release_response
.url() .url()
.path() .path()
@ -1067,7 +1072,7 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
} }
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> { pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
let mut req = reqwest::Client::new().post(url); let mut req = create_http_client_async().post(url);
if !header.is_empty() { if !header.is_empty() {
let tmp: Vec<&str> = header.split(": ").collect(); let tmp: Vec<&str> = header.split(": ").collect();
if tmp.len() == 2 { if tmp.len() == 2 {
@ -1084,6 +1089,65 @@ pub async fn post_request_sync(url: String, body: String, header: &str) -> Resul
post_request(url, body, header).await post_request(url, body, header).await
} }
#[tokio::main(flavor = "current_thread")]
pub async fn http_request_sync(
url: String,
method: String,
body: Option<String>,
header: String,
) -> ResultType<String> {
let http_client = create_http_client_async();
let mut http_client = match method.as_str() {
"get" => http_client.get(url),
"post" => http_client.post(url),
"put" => http_client.put(url),
"delete" => http_client.delete(url),
_ => return Err(anyhow!("The HTTP request method is not supported!")),
};
let v = serde_json::from_str(header.as_str())?;
if let Value::Object(obj) = v {
for (key, value) in obj.iter() {
http_client = http_client.header(key, value.as_str().unwrap_or_default());
}
} else {
return Err(anyhow!("HTTP header information parsing failed!"));
}
if let Some(b) = body {
http_client = http_client.body(b);
}
let response = http_client
.timeout(std::time::Duration::from_secs(12))
.send()
.await?;
// Serialize response headers
let mut response_headers = serde_json::map::Map::new();
for (key, value) in response.headers() {
response_headers.insert(
key.to_string(),
serde_json::json!(value.to_str().unwrap_or("")),
);
}
let status_code = response.status().as_u16();
let response_body = response.text().await?;
// Construct the JSON object
let mut result = serde_json::map::Map::new();
result.insert("status_code".to_string(), serde_json::json!(status_code));
result.insert(
"headers".to_string(),
serde_json::Value::Object(response_headers),
);
result.insert("body".to_string(), serde_json::json!(response_body));
// Convert map to JSON string
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
}
#[inline] #[inline]
pub fn make_privacy_mode_msg_with_details( pub fn make_privacy_mode_msg_with_details(
state: back_notification::PrivacyModeState, state: back_notification::PrivacyModeState,

View File

@ -1,5 +1,9 @@
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use hbb_common::{
use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType}; bail,
base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _},
sodiumoxide::crypto::sign,
ResultType,
};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]

View File

@ -882,6 +882,21 @@ impl InvokeUiSession for FlutterHandler {
); );
} }
fn is_multi_ui_session(&self) -> bool {
self.session_handlers.read().unwrap().len() > 1
}
fn set_current_display(&self, disp_idx: i32) {
if self.is_multi_ui_session() {
return;
}
self.push_event(
"follow_current_display",
&[("display_idx", &disp_idx.to_string())],
&[],
);
}
fn on_connected(&self, _conn_type: ConnType) {} fn on_connected(&self, _conn_type: ConnType) {}
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) {

View File

@ -213,6 +213,14 @@ pub fn session_refresh(session_id: SessionID, display: usize) {
} }
} }
pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn<bool> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
SyncReturn(session.is_multi_ui_session())
} else {
SyncReturn(false)
}
}
pub fn session_record_screen( pub fn session_record_screen(
session_id: SessionID, session_id: SessionID,
start: bool, start: bool,
@ -749,6 +757,10 @@ pub fn main_get_async_status() -> String {
get_async_job_status() get_async_job_status()
} }
pub fn main_get_http_status(url: String) -> Option<String> {
get_async_http_status(url)
}
pub fn main_get_option(key: String) -> String { pub fn main_get_option(key: String) -> String {
get_option(key) get_option(key)
} }
@ -797,14 +809,18 @@ pub fn main_set_options(json: String) {
} }
} }
pub fn main_test_if_valid_server(server: String) -> String { pub fn main_test_if_valid_server(server: String, test_with_proxy: bool) -> String {
test_if_valid_server(server) test_if_valid_server(server, test_with_proxy)
} }
pub fn main_set_socks(proxy: String, username: String, password: String) { pub fn main_set_socks(proxy: String, username: String, password: String) {
set_socks(proxy, username, password) set_socks(proxy, username, password)
} }
pub fn main_get_proxy_status() -> bool {
get_proxy_status()
}
pub fn main_get_socks() -> Vec<String> { pub fn main_get_socks() -> Vec<String> {
get_socks() get_socks()
} }
@ -878,9 +894,8 @@ pub fn main_get_api_server() -> String {
get_api_server() get_api_server()
} }
// This function doesn't seem to be used. pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
pub fn main_post_request(url: String, body: String, header: String) { http_request(url, method, body, header)
post_request(url, body, header)
} }
pub fn main_get_local_option(key: String) -> SyncReturn<String> { pub fn main_get_local_option(key: String) -> SyncReturn<String> {

View File

@ -4,8 +4,11 @@ use serde_json::{Map, Value};
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
pub mod account; pub mod account;
mod http_client;
pub mod record_upload; pub mod record_upload;
pub mod sync; pub mod sync;
pub use http_client::create_http_client;
pub use http_client::create_http_client_async;
#[derive(Debug)] #[derive(Debug)]
pub enum HbbHttpResponse<T> { pub enum HbbHttpResponse<T> {

View File

@ -1,4 +1,5 @@
use super::HbbHttpResponse; use super::HbbHttpResponse;
use crate::hbbs_http::create_http_client;
use hbb_common::{config::LocalConfig, log, ResultType}; use hbb_common::{config::LocalConfig, log, ResultType};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -130,7 +131,7 @@ impl Default for UserStatus {
impl OidcSession { impl OidcSession {
fn new() -> Self { fn new() -> Self {
Self { Self {
client: Client::new(), client: create_http_client(),
state_msg: REQUESTING_ACCOUNT_AUTH, state_msg: REQUESTING_ACCOUNT_AUTH,
failed_msg: "".to_owned(), failed_msg: "".to_owned(),
code_url: None, code_url: None,
@ -168,7 +169,7 @@ impl OidcSession {
id: &str, id: &str,
uuid: &str, uuid: &str,
) -> ResultType<HbbHttpResponse<AuthBody>> { ) -> ResultType<HbbHttpResponse<AuthBody>> {
let url = reqwest::Url::parse_with_params( let url = Url::parse_with_params(
&format!("{}/api/oidc/auth-query", api_server), &format!("{}/api/oidc/auth-query", api_server),
&[("code", code), ("id", id), ("uuid", uuid)], &[("code", code), ("id", id), ("uuid", uuid)],
)?; )?;

View File

@ -0,0 +1,71 @@
use hbb_common::config::Config;
use hbb_common::log::info;
use hbb_common::proxy::{Proxy, ProxyScheme};
use reqwest::blocking::Client as SyncClient;
use reqwest::Client as AsyncClient;
macro_rules! configure_http_client {
($builder:expr, $Client: ty) => {{
let mut builder = $builder;
let client = if let Some(conf) = Config::get_socks() {
let proxy_result = Proxy::from_conf(&conf, None);
match proxy_result {
Ok(proxy) => {
let proxy_setup = match &proxy.intercept {
ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::http(format!("http://{}", host))},
ProxyScheme::Https { host, .. } => {reqwest::Proxy::https(format!("https://{}", host))},
ProxyScheme::Socks5 { addr, .. } => { reqwest::Proxy::all(&format!("socks5://{}", addr)) }
};
match proxy_setup {
Ok(p) => {
builder = builder.proxy(p);
if let Some(auth) = proxy.intercept.maybe_auth() {
let basic_auth =
format!("Basic {}", auth.get_basic_authorization());
builder = builder.default_headers(
vec![(
reqwest::header::PROXY_AUTHORIZATION,
basic_auth.parse().unwrap(),
)]
.into_iter()
.collect(),
);
}
builder.build().unwrap_or_else(|e| {
info!("Failed to create a proxied client: {}", e);
<$Client>::new()
})
}
Err(e) => {
info!("Failed to set up proxy: {}", e);
<$Client>::new()
}
}
}
Err(e) => {
info!("Failed to configure proxy: {}", e);
<$Client>::new()
}
}
} else {
builder.build().unwrap_or_else(|e| {
info!("Failed to create a client: {}", e);
<$Client>::new()
})
};
client
}};
}
pub fn create_http_client() -> SyncClient {
let builder = SyncClient::builder();
configure_http_client!(builder, SyncClient)
}
pub fn create_http_client_async() -> AsyncClient {
let builder = AsyncClient::builder();
configure_http_client!(builder, AsyncClient)
}

View File

@ -1,3 +1,4 @@
use crate::hbbs_http::create_http_client;
use bytes::Bytes; use bytes::Bytes;
use hbb_common::{bail, config::Config, lazy_static, log, ResultType}; use hbb_common::{bail, config::Config, lazy_static, log, ResultType};
use reqwest::blocking::{Body, Client}; use reqwest::blocking::{Body, Client};
@ -25,7 +26,7 @@ pub fn is_enable() -> bool {
pub fn run(rx: Receiver<RecordState>) { pub fn run(rx: Receiver<RecordState>) {
let mut uploader = RecordUploader { let mut uploader = RecordUploader {
client: Client::new(), client: create_http_client(),
api_server: crate::get_api_server( api_server: crate::get_api_server(
Config::get_option("api-server"), Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"), Config::get_option("custom-rendezvous-server"),

View File

@ -233,6 +233,7 @@ pub enum Data {
ControlledSessionCount(usize), ControlledSessionCount(usize),
CmErr(String), CmErr(String),
CheckHwcodec, CheckHwcodec,
VideoConnCount(Option<usize>),
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
@ -382,6 +383,15 @@ async fn handle(data: Data, stream: &mut Connection) {
log::info!("socks updated"); log::info!("socks updated");
} }
}, },
Data::VideoConnCount(None) => {
let n = crate::server::AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|x| x.1 == crate::server::AuthConnType::Remote)
.count();
allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await);
}
Data::Config((name, value)) => match value { Data::Config((name, value)) => match value {
None => { None => {
let value; let value;
@ -904,6 +914,9 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
Ok(()) Ok(())
} }
pub fn get_proxy_status() -> bool {
Config::get_socks().is_some()
}
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
pub async fn test_rendezvous_server() -> ResultType<()> { pub async fn test_rendezvous_server() -> ResultType<()> {
let mut c = connect(1000, "").await?; let mut c = connect(1000, "").await?;

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "فارغ"), ("Empty", "فارغ"),
("Invalid folder name", "اسم المجلد غير صحيح"), ("Invalid folder name", "اسم المجلد غير صحيح"),
("Socks5 Proxy", "وكيل Socks5"), ("Socks5 Proxy", "وكيل Socks5"),
("Socks5/Http(s) Proxy", "وكيل Socks5/Http(s)"),
("Discovered", "المكتشفة"), ("Discovered", "المكتشفة"),
("install_daemon_tip", "للبدء مع بدء تشغيل النظام. تحتاج الى تثبيت خدمة النظام."), ("install_daemon_tip", "للبدء مع بدء تشغيل النظام. تحتاج الى تثبيت خدمة النظام."),
("Remote ID", "المعرف البعيد"), ("Remote ID", "المعرف البعيد"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", ""), ("Empty", ""),
("Invalid folder name", ""), ("Invalid folder name", ""),
("Socks5 Proxy", "Socks5 прокси"), ("Socks5 Proxy", "Socks5 прокси"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"),
("Discovered", ""), ("Discovered", ""),
("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), ("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."),
("Remote ID", ""), ("Remote ID", ""),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Buit"), ("Empty", "Buit"),
("Invalid folder name", "Nom de carpeta incorrecte"), ("Invalid folder name", "Nom de carpeta incorrecte"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Descobert"), ("Discovered", "Descobert"),
("install_daemon_tip", ""), ("install_daemon_tip", ""),
("Remote ID", "ID remot"), ("Remote ID", "ID remot"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "空空如也"), ("Empty", "空空如也"),
("Invalid folder name", "无效文件夹名称"), ("Invalid folder name", "无效文件夹名称"),
("Socks5 Proxy", "Socks5 代理"), ("Socks5 Proxy", "Socks5 代理"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理"),
("Default protocol and port are Socks5 and 1080", "默认代理协议及端口为Socks5和1080"),
("Discovered", "已发现"), ("Discovered", "已发现"),
("install_daemon_tip", "为了开机启动,请安装系统服务。"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"),
("Remote ID", "远程 ID"), ("Remote ID", "远程 ID"),
@ -602,5 +604,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "打开 Web 控制台以执行更多操作"), ("ab_web_console_tip", "打开 Web 控制台以执行更多操作"),
("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"), ("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"),
("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"), ("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Prázdné"), ("Empty", "Prázdné"),
("Invalid folder name", "Neplatný název složky"), ("Invalid folder name", "Neplatný název složky"),
("Socks5 Proxy", "Socks5 proxy"), ("Socks5 Proxy", "Socks5 proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"),
("Discovered", "Objeveno"), ("Discovered", "Objeveno"),
("install_daemon_tip", "Pokud má být spouštěno při startu systému, je třeba nainstalovat systémovou službu."), ("install_daemon_tip", "Pokud má být spouštěno při startu systému, je třeba nainstalovat systémovou službu."),
("Remote ID", "Vzdálené ID"), ("Remote ID", "Vzdálené ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Více na webové konzoli"), ("ab_web_console_tip", "Více na webové konzoli"),
("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"), ("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"),
("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."), ("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."),
("Follow remote cursor", "Sledovat dálkový kurzor"),
("Follow remote window focus", "Sledovat zaměření vzdáleného okna"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Tom"), ("Empty", "Tom"),
("Invalid folder name", "Ugyldigt mappenavn"), ("Invalid folder name", "Ugyldigt mappenavn"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Fundet"), ("Discovered", "Fundet"),
("install_daemon_tip", "For at starte efter PC'en er startet op, skal du installere systemtjenesten"), ("install_daemon_tip", "For at starte efter PC'en er startet op, skal du installere systemtjenesten"),
("Remote ID", "Fjern-ID"), ("Remote ID", "Fjern-ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Keine Einträge"), ("Empty", "Keine Einträge"),
("Invalid folder name", "Ungültiger Ordnername"), ("Invalid folder name", "Ungültiger Ordnername"),
("Socks5 Proxy", "SOCKS5-Proxy"), ("Socks5 Proxy", "SOCKS5-Proxy"),
("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S)-Proxy"),
("Discovered", "Im LAN erkannt"), ("Discovered", "Im LAN erkannt"),
("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."),
("Remote ID", "Entfernte ID"), ("Remote ID", "Entfernte ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Mehr über Webkonsole"), ("ab_web_console_tip", "Mehr über Webkonsole"),
("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"), ("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"),
("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."), ("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."),
("Follow remote cursor", "Dem entfernten Cursor folgen"),
("Follow remote window focus", "Dem Fokus des entfernten Fensters folgen"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Άδειο"), ("Empty", "Άδειο"),
("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"),
("Socks5 Proxy", "Διαμεσολαβητής Socks5"), ("Socks5 Proxy", "Διαμεσολαβητής Socks5"),
("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"),
("Discovered", "Ανακαλύφθηκαν"), ("Discovered", "Ανακαλύφθηκαν"),
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"),
("Remote ID", "Απομακρυσμένο ID"), ("Remote ID", "Απομακρυσμένο ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -60,6 +60,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Add to Favorites", "Add to favorites"), ("Add to Favorites", "Add to favorites"),
("Remove from Favorites", "Remove from favorites"), ("Remove from Favorites", "Remove from favorites"),
("Socks5 Proxy", "Socks5 proxy"), ("Socks5 Proxy", "Socks5 proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"),
("Default protocol and port are Socks5 and 1080", "Default protocol and port are Socks5 and 1080"),
("install_daemon_tip", "For starting on boot, you need to install system service."), ("install_daemon_tip", "For starting on boot, you need to install system service."),
("Are you sure to close the connection?", "Are you sure you want to close the connection?"), ("Are you sure to close the connection?", "Are you sure you want to close the connection?"),
("One-Finger Tap", "One-finger tap"), ("One-Finger Tap", "One-finger tap"),
@ -220,5 +222,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "More on web console"), ("ab_web_console_tip", "More on web console"),
("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"), ("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"),
("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."), ("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Malplena"), ("Empty", "Malplena"),
("Invalid folder name", "Dosiernomo nevalida"), ("Invalid folder name", "Dosiernomo nevalida"),
("Socks5 Proxy", "Socks5 prokura servilo"), ("Socks5 Proxy", "Socks5 prokura servilo"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"),
("Discovered", "Malkovritaj"), ("Discovered", "Malkovritaj"),
("install_daemon_tip", ""), ("install_daemon_tip", ""),
("Remote ID", "Fora identigilo"), ("Remote ID", "Fora identigilo"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Vacío"), ("Empty", "Vacío"),
("Invalid folder name", "Nombre de carpeta incorrecto"), ("Invalid folder name", "Nombre de carpeta incorrecto"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Descubierto"), ("Discovered", "Descubierto"),
("install_daemon_tip", "Para comenzar en el encendido, debe instalar el servicio del sistema."), ("install_daemon_tip", "Para comenzar en el encendido, debe instalar el servicio del sistema."),
("Remote ID", "ID remoto"), ("Remote ID", "ID remoto"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Más en consola web"), ("ab_web_console_tip", "Más en consola web"),
("allow-only-conn-window-open-tip", "Permitir la conexión solo si la ventana RusDesk está abierta"), ("allow-only-conn-window-open-tip", "Permitir la conexión solo si la ventana RusDesk está abierta"),
("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."), ("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", ""), ("Empty", ""),
("Invalid folder name", ""), ("Invalid folder name", ""),
("Socks5 Proxy", "Socks5 proksi"), ("Socks5 Proxy", "Socks5 proksi"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"),
("Discovered", ""), ("Discovered", ""),
("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."), ("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."),
("Remote ID", ""), ("Remote ID", ""),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "موردی وجود ندارد"), ("Empty", "موردی وجود ندارد"),
("Invalid folder name", "نام پوشه نامعتبر است"), ("Invalid folder name", "نام پوشه نامعتبر است"),
("Socks5 Proxy", "Socks5 پروکسی"), ("Socks5 Proxy", "Socks5 پروکسی"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) پروکسی"),
("Discovered", "پیدا شده"), ("Discovered", "پیدا شده"),
("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"),
("Remote ID", "شناسه راه دور"), ("Remote ID", "شناسه راه دور"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"), ("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Vide"), ("Empty", "Vide"),
("Invalid folder name", "Nom de dossier invalide"), ("Invalid folder name", "Nom de dossier invalide"),
("Socks5 Proxy", "Socks5 Agents"), ("Socks5 Proxy", "Socks5 Agents"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Agents"),
("Discovered", "Découvert"), ("Discovered", "Découvert"),
("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."), ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."),
("Remote ID", "ID de l'appareil distant"), ("Remote ID", "ID de l'appareil distant"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", ""), ("Empty", ""),
("Invalid folder name", ""), ("Invalid folder name", ""),
("Socks5 Proxy", "פרוקסי Socks5"), ("Socks5 Proxy", "פרוקסי Socks5"),
("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"),
("Discovered", ""), ("Discovered", ""),
("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."),
("Remote ID", ""), ("Remote ID", ""),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -602,5 +602,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Više na web konzoli"), ("ab_web_console_tip", "Više na web konzoli"),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Üres"), ("Empty", "Üres"),
("Invalid folder name", "Helytelen mappa név"), ("Invalid folder name", "Helytelen mappa név"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Felfedezett"), ("Discovered", "Felfedezett"),
("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"), ("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"),
("Remote ID", "Távoli azonosító"), ("Remote ID", "Távoli azonosító"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Kosong"), ("Empty", "Kosong"),
("Invalid folder name", "Nama folder tidak valid"), ("Invalid folder name", "Nama folder tidak valid"),
("Socks5 Proxy", "Proksi Socks5"), ("Socks5 Proxy", "Proksi Socks5"),
("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"),
("Discovered", "Telah ditemukan"), ("Discovered", "Telah ditemukan"),
("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."), ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."),
("Remote ID", "ID Remote"), ("Remote ID", "ID Remote"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"),
("Set Password", "Imposta password"), ("Set Password", "Imposta password"),
("OS Password", "Password sistema operativo"), ("OS Password", "Password sistema operativo"),
("install_tip", "A causa del controllo account uUtente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), ("install_tip", "A causa del Controllo Account Utente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."),
("Click to upgrade", "Aggiorna"), ("Click to upgrade", "Aggiorna"),
("Click to download", "Download"), ("Click to download", "Download"),
("Click to update", "Aggiorna"), ("Click to update", "Aggiorna"),
@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Vuoto"), ("Empty", "Vuoto"),
("Invalid folder name", "Nome della cartella non valido"), ("Invalid folder name", "Nome della cartella non valido"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Rilevate"), ("Discovered", "Rilevate"),
("install_daemon_tip", "Per avviare il programma all'accensione, è necessario installarlo come servizio di sistema."), ("install_daemon_tip", "Per avviare il programma all'accensione, è necessario installarlo come servizio di sistema."),
("Remote ID", "ID remoto"), ("Remote ID", "ID remoto"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Altre info sulla console web"), ("ab_web_console_tip", "Altre info sulla console web"),
("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"),
("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."),
("Follow remote cursor", "Segui cursore remoto"),
("Follow remote window focus", "Segui focus finestra remota"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", ""), ("Empty", ""),
("Invalid folder name", "無効なフォルダ名"), ("Invalid folder name", "無効なフォルダ名"),
("Socks5 Proxy", "SOCKS5プロキシ"), ("Socks5 Proxy", "SOCKS5プロキシ"),
("Socks5/Http(s) Proxy", "Socks5/Http(s)プロキシ"),
("Discovered", "探知済み"), ("Discovered", "探知済み"),
("install_daemon_tip", "起動時に開始するには、システムサービスをインストールする必要があります。"), ("install_daemon_tip", "起動時に開始するには、システムサービスをインストールする必要があります。"),
("Remote ID", "リモートのID"), ("Remote ID", "リモートのID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "비어 있음"), ("Empty", "비어 있음"),
("Invalid folder name", "유효하지 않은 폴더명"), ("Invalid folder name", "유효하지 않은 폴더명"),
("Socks5 Proxy", "Socks5 프록시"), ("Socks5 Proxy", "Socks5 프록시"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) 프록시"),
("Discovered", "찾음"), ("Discovered", "찾음"),
("install_daemon_tip", "부팅된 이후 시스템 서비스에 설치해야 합니다."), ("install_daemon_tip", "부팅된 이후 시스템 서비스에 설치해야 합니다."),
("Remote ID", "원격 ID"), ("Remote ID", "원격 ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Бос"), ("Empty", "Бос"),
("Invalid folder name", "Бұрыс бума атауы"), ("Invalid folder name", "Бұрыс бума атауы"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Табылды"), ("Discovered", "Табылды"),
("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."),
("Remote ID", "Қашықтағы ID"), ("Remote ID", "Қашықтағы ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Tuščia"), ("Empty", "Tuščia"),
("Invalid folder name", "Neteisingas aplanko pavadinimas"), ("Invalid folder name", "Neteisingas aplanko pavadinimas"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Aptikta tinkle"), ("Discovered", "Aptikta tinkle"),
("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"), ("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"),
("Remote ID", "Nuotolinis ID"), ("Remote ID", "Nuotolinis ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Tukšs"), ("Empty", "Tukšs"),
("Invalid folder name", "Nederīgs mapes nosaukums"), ("Invalid folder name", "Nederīgs mapes nosaukums"),
("Socks5 Proxy", "Socks5 starpniekserveris"), ("Socks5 Proxy", "Socks5 starpniekserveris"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) starpniekserveris"),
("Discovered", "Atklāts"), ("Discovered", "Atklāts"),
("install_daemon_tip", "Lai palaistu pie startēšanas, ir jāinstalē sistēmas serviss."), ("install_daemon_tip", "Lai palaistu pie startēšanas, ir jāinstalē sistēmas serviss."),
("Remote ID", "Attālais ID"), ("Remote ID", "Attālais ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Vairāk par tīmekļa konsoli"), ("ab_web_console_tip", "Vairāk par tīmekļa konsoli"),
("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"), ("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"),
("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."), ("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."),
("Follow remote cursor", "Sekot attālajam kursoram"),
("Follow remote window focus", "Sekot attālā loga fokusam"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Tom"), ("Empty", "Tom"),
("Invalid folder name", "Ugyldig mappenavn"), ("Invalid folder name", "Ugyldig mappenavn"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Oppdaget"), ("Discovered", "Oppdaget"),
("install_daemon_tip", "For å starte når PC'en har startet opp, må du installere systemtjenesten"), ("install_daemon_tip", "For å starte når PC'en har startet opp, må du installere systemtjenesten"),
("Remote ID", "Fjern-ID"), ("Remote ID", "Fjern-ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Leeg"), ("Empty", "Leeg"),
("Invalid folder name", "Ongeldige mapnaam"), ("Invalid folder name", "Ongeldige mapnaam"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Ontdekt"), ("Discovered", "Ontdekt"),
("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet u de systeemservice installeren."), ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet u de systeemservice installeren."),
("Remote ID", "Externe ID"), ("Remote ID", "Externe ID"),
@ -601,6 +602,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Everyone", "Iedereen"), ("Everyone", "Iedereen"),
("ab_web_console_tip", "Meer over de webconsole"), ("ab_web_console_tip", "Meer over de webconsole"),
("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"), ("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", "Geen fysieke schermen, geen privémodus nodig."),
("Follow remote cursor", "Volg de cursor op afstand"),
("Follow remote window focus", "Volg de focus van het venster op afstand"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Pusto"), ("Empty", "Pusto"),
("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Wykryte"), ("Discovered", "Wykryte"),
("install_daemon_tip", "By uruchomić RustDesk przy starcie systemu, musisz zainstalować usługę systemową."), ("install_daemon_tip", "By uruchomić RustDesk przy starcie systemu, musisz zainstalować usługę systemową."),
("Remote ID", "Zdalne ID"), ("Remote ID", "Zdalne ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Więcej w konsoli web"), ("ab_web_console_tip", "Więcej w konsoli web"),
("allow-only-conn-window-open-tip", "Zezwalaj na połączenie tylko wtedy, gdy okno RustDesk jest otwarte"), ("allow-only-conn-window-open-tip", "Zezwalaj na połączenie tylko wtedy, gdy okno RustDesk jest otwarte"),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Vazio"), ("Empty", "Vazio"),
("Invalid folder name", "Nome de diretório inválido"), ("Invalid folder name", "Nome de diretório inválido"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Descoberto"), ("Discovered", "Descoberto"),
("install_daemon_tip", "Para inicialização junto do sistema, deve instalar o serviço de sistema."), ("install_daemon_tip", "Para inicialização junto do sistema, deve instalar o serviço de sistema."),
("Remote ID", "ID Remoto"), ("Remote ID", "ID Remoto"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Vazio"), ("Empty", "Vazio"),
("Invalid folder name", "Nome de diretório inválido"), ("Invalid folder name", "Nome de diretório inválido"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Descoberto"), ("Discovered", "Descoberto"),
("install_daemon_tip", "Para inicialização junto ao sistema, você deve instalar o serviço de sistema."), ("install_daemon_tip", "Para inicialização junto ao sistema, você deve instalar o serviço de sistema."),
("Remote ID", "ID Remoto"), ("Remote ID", "ID Remoto"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Gol"), ("Empty", "Gol"),
("Invalid folder name", "Denumire folder nevalidă"), ("Invalid folder name", "Denumire folder nevalidă"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Descoperite"), ("Discovered", "Descoperite"),
("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."),
("Remote ID", "ID dispozitiv la distanță"), ("Remote ID", "ID dispozitiv la distanță"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Пусто"), ("Empty", "Пусто"),
("Invalid folder name", "Недопустимое имя папки"), ("Invalid folder name", "Недопустимое имя папки"),
("Socks5 Proxy", "SOCKS5-прокси"), ("Socks5 Proxy", "SOCKS5-прокси"),
("Socks5/Http(s) Proxy", "Socks5/Http(s)-прокси"),
("Discovered", "Найдено"), ("Discovered", "Найдено"),
("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"), ("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"),
("Remote ID", "Удалённый ID"), ("Remote ID", "Удалённый ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Больше в веб-консоли"), ("ab_web_console_tip", "Больше в веб-консоли"),
("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"),
("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."),
("Follow remote cursor", "Следовать за удалённым курсором"),
("Follow remote window focus", "Следовать за фокусом удалённого окна"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Prázdne"), ("Empty", "Prázdne"),
("Invalid folder name", "Neplatný názov adresára"), ("Invalid folder name", "Neplatný názov adresára"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Objavené"), ("Discovered", "Objavené"),
("install_daemon_tip", "Ak chcete, aby sa spúšťal pri štarte systému, musíte nainštalovať systémovú službu."), ("install_daemon_tip", "Ak chcete, aby sa spúšťal pri štarte systému, musíte nainštalovať systémovú službu."),
("Remote ID", "Vzdialené ID"), ("Remote ID", "Vzdialené ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Viac na webovej konzole"), ("ab_web_console_tip", "Viac na webovej konzole"),
("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"), ("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"),
("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."), ("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."),
("Follow remote cursor", "Nasledovať vzdialený kurzor"),
("Follow remote window focus", "Nasledovať vzdialené zameranie okna"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Prazno"), ("Empty", "Prazno"),
("Invalid folder name", "Napačno ime mape"), ("Invalid folder name", "Napačno ime mape"),
("Socks5 Proxy", "Socks5 posredniški strežnik"), ("Socks5 Proxy", "Socks5 posredniški strežnik"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) posredniški strežnik"),
("Discovered", "Odkriti"), ("Discovered", "Odkriti"),
("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"),
("Remote ID", "Oddaljeni ID"), ("Remote ID", "Oddaljeni ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Bosh"), ("Empty", "Bosh"),
("Invalid folder name", "Emri i dosjes i pavlefshëm"), ("Invalid folder name", "Emri i dosjes i pavlefshëm"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "I pambuluar"), ("Discovered", "I pambuluar"),
("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"),
("Remote ID", "ID në distancë"), ("Remote ID", "ID në distancë"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Prazno"), ("Empty", "Prazno"),
("Invalid folder name", "Pogrešno ime direktorijuma"), ("Invalid folder name", "Pogrešno ime direktorijuma"),
("Socks5 Proxy", "Socks5 proksi"), ("Socks5 Proxy", "Socks5 proksi"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"),
("Discovered", "Otkriveno"), ("Discovered", "Otkriveno"),
("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."),
("Remote ID", "Udaljeni ID"), ("Remote ID", "Udaljeni ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Tom"), ("Empty", "Tom"),
("Invalid folder name", "Ogiltigt mappnamn"), ("Invalid folder name", "Ogiltigt mappnamn"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Upptäckt"), ("Discovered", "Upptäckt"),
("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."),
("Remote ID", "Fjärr ID"), ("Remote ID", "Fjärr ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", ""), ("Empty", ""),
("Invalid folder name", ""), ("Invalid folder name", ""),
("Socks5 Proxy", ""), ("Socks5 Proxy", ""),
("Socks5/Http(s) Proxy", ""),
("Discovered", ""), ("Discovered", ""),
("install_daemon_tip", ""), ("install_daemon_tip", ""),
("Remote ID", ""), ("Remote ID", ""),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "ว่างเปล่า"), ("Empty", "ว่างเปล่า"),
("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"),
("Socks5 Proxy", "พรอกซี Socks5"), ("Socks5 Proxy", "พรอกซี Socks5"),
("Socks5/Http(s) Proxy", "พรอกซี Socks5/Http(s)"),
("Discovered", "ค้นพบ"), ("Discovered", "ค้นพบ"),
("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"),
("Remote ID", "ID ปลายทาง"), ("Remote ID", "ID ปลายทาง"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Boş"), ("Empty", "Boş"),
("Invalid folder name", "Geçersiz klasör adı"), ("Invalid folder name", "Geçersiz klasör adı"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
("Discovered", "Keşfedilenler"), ("Discovered", "Keşfedilenler"),
("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."), ("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."),
("Remote ID", "Uzak ID"), ("Remote ID", "Uzak ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "空空如也"), ("Empty", "空空如也"),
("Invalid folder name", "資料夾名稱無效"), ("Invalid folder name", "資料夾名稱無效"),
("Socks5 Proxy", "Socks5 代理伺服器"), ("Socks5 Proxy", "Socks5 代理伺服器"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理伺服器"),
("Discovered", "已探索"), ("Discovered", "已探索"),
("install_daemon_tip", "若要在開機時啟動,您需要安裝系統服務。"), ("install_daemon_tip", "若要在開機時啟動,您需要安裝系統服務。"),
("Remote ID", "遠端 ID"), ("Remote ID", "遠端 ID"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"),
("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Пусто"), ("Empty", "Пусто"),
("Invalid folder name", "Неприпустима назва теки"), ("Invalid folder name", "Неприпустима назва теки"),
("Socks5 Proxy", "Проксі-сервер Socks5"), ("Socks5 Proxy", "Проксі-сервер Socks5"),
("Socks5/Http(s) Proxy", "Проксі-сервер Socks5/Http(s)"),
("Discovered", "Знайдено"), ("Discovered", "Знайдено"),
("install_daemon_tip", "Для запуску під час завантаження, вам необхідно встановити системну службу"), ("install_daemon_tip", "Для запуску під час завантаження, вам необхідно встановити системну службу"),
("Remote ID", "Віддалений ідентифікатор"), ("Remote ID", "Віддалений ідентифікатор"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "Детальніше про веб-консоль"), ("ab_web_console_tip", "Детальніше про веб-консоль"),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "Trống"), ("Empty", "Trống"),
("Invalid folder name", "Tên thư mục không hợp lệ"), ("Invalid folder name", "Tên thư mục không hợp lệ"),
("Socks5 Proxy", "Proxy Socks5"), ("Socks5 Proxy", "Proxy Socks5"),
("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"),
("Discovered", "Đuợc phát hiện"), ("Discovered", "Đuợc phát hiện"),
("install_daemon_tip", "Để chạy lúc khởi động máy, bạn cần phải cài dịch vụ hệ thống."), ("install_daemon_tip", "Để chạy lúc khởi động máy, bạn cần phải cài dịch vụ hệ thống."),
("Remote ID", "ID từ xa"), ("Remote ID", "ID từ xa"),
@ -602,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -3,7 +3,9 @@ mod keyboard;
/// cbindgen:ignore /// cbindgen:ignore
pub mod platform; pub mod platform;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; pub use platform::{
get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, start_os_service,
};
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
/// cbindgen:ignore /// cbindgen:ignore
mod server; mod server;
@ -36,15 +38,15 @@ pub mod flutter;
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
pub mod flutter_ffi; pub mod flutter_ffi;
use common::*; use common::*;
mod auth_2fa;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub mod cli; pub mod cli;
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
pub mod core_main; pub mod core_main;
mod lang;
mod custom_server; mod custom_server;
mod lang;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
mod port_forward; mod port_forward;
mod auth_2fa;
#[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]

Some files were not shown because too many files have changed in this diff Show More