diff --git a/.cargo/config.toml b/.cargo/config.toml
index 0e9fd44a90db..42a4adb55e1a 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,8 +1,16 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
-rustflags = ["-Ctarget-feature=+crt-static"]
+rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
]
+#[target.'cfg(target_os="linux")']
+# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
+# this is unlikely to help also, because the other so files still use libc dynamically
+#rustflags = [
+# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
+#]
+[net]
+git-fetch-with-cli = true
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index d8781bb0c6be..dbb8ed8a92e9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -26,8 +26,8 @@ body:
- type: input
id: os
attributes:
- label: Operating system(s) on local side and remote side
- description: What operating system(s) do you see this bug on? local side -> remote side.
+ label: Operating system(s) on local (controlling) side and remote (controlled) side
+ description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: |
Windows 10 -> osx
validations:
@@ -35,8 +35,8 @@ body:
- type: input
id: version
attributes:
- label: RustDesk Version(s) on local side and remote side
- description: What RustDesk version(s) do you see this bug on? local side -> remote side.
+ label: RustDesk Version(s) on local (controlling) side and remote (controlled) side
+ description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: |
1.1.9 -> 1.1.8
validations:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000000..56258e4e0a90
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ - package-ecosystem: "gitsubmodule"
+ directory: "/"
+ target-branch: "master"
+ schedule:
+ interval: "daily"
+ commit-message:
+ prefix: "Git submodule"
+ labels:
+ - "dependencies"
diff --git a/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
new file mode 100644
index 000000000000..9b8ea26906e9
--- /dev/null
+++ b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
@@ -0,0 +1,42 @@
+diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
+index 7e634cd2aa..c1e9acc295 100644
+--- a/packages/flutter/lib/src/material/dropdown_menu.dart
++++ b/packages/flutter/lib/src/material/dropdown_menu.dart
+@@ -475,7 +475,7 @@ class _DropdownMenuState extends State> {
+ final GlobalKey _leadingKey = GlobalKey();
+ late List buttonItemKeys;
+ final MenuController _controller = MenuController();
+- late bool _enableFilter;
++ bool _enableFilter = false;
+ late List> filteredEntries;
+ List? _initialMenu;
+ int? currentHighlight;
+@@ -524,6 +524,11 @@ class _DropdownMenuState extends State> {
+ }
+ _localTextEditingController = widget.controller ?? TextEditingController();
+ }
++ if (oldWidget.enableFilter != widget.enableFilter) {
++ if (!widget.enableFilter) {
++ _enableFilter = false;
++ }
++ }
+ if (oldWidget.enableSearch != widget.enableSearch) {
+ if (!widget.enableSearch) {
+ currentHighlight = null;
+@@ -663,6 +668,7 @@ class _DropdownMenuState extends State> {
+ );
+ currentHighlight = widget.enableSearch ? i : null;
+ widget.onSelected?.call(entry.value);
++ _enableFilter = false;
+ }
+ : null,
+ requestFocusOnHover: false,
+@@ -735,6 +741,8 @@ class _DropdownMenuState extends State> {
+ if (_enableFilter) {
+ filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
+ ?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
++ } else {
++ filteredEntries = widget.dropdownMenuEntries;
+ }
+
+ if (widget.enableSearch) {
diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml
index fbaf459a4d81..2d5affeef520 100644
--- a/.github/workflows/bridge.yml
+++ b/.github/workflows/bridge.yml
@@ -6,7 +6,8 @@ on:
workflow_call:
env:
- FLUTTER_VERSION: "3.16.9"
+ CARGO_EXPAND_VERSION: "1.0.95"
+ FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -25,6 +26,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Install prerequisites
run: |
@@ -73,12 +76,14 @@ jobs:
- name: Install flutter rust bridge deps
shell: bash
run: |
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- pushd flutter && flutter pub get && popd
+ cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
+ cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
+ pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
- name: Run flutter rust bridge
run: |
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
+ ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
+ cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
- name: Upload Artifact
uses: actions/upload-artifact@master
@@ -89,3 +94,5 @@ jobs:
./src/bridge_generated.io.rs
./flutter/lib/generated_bridge.dart
./flutter/lib/generated_bridge.freezed.dart
+ ./flutter/macos/Runner/bridge_generated.h
+ ./flutter/ios/Runner/bridge_generated.h
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 90f312968e07..690e3cf44fb6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
- # vcpkg version: 2024.06.15
+ # vcpkg version: 2024.11.16
# for multiarch gcc compatibility
- VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
+ VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
on:
workflow_dispatch:
@@ -45,6 +45,8 @@ jobs:
# steps:
# - name: Checkout source code
# uses: actions/checkout@v3
+ # with:
+ # submodules: recursive
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
# uses: actions-rs/toolchain@v1
@@ -92,6 +94,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Install prerequisites
shell: bash
diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml
index 2669f06d7a4a..a795bfaeda05 100644
--- a/.github/workflows/flutter-build.yml
+++ b/.github/workflows/flutter-build.yml
@@ -18,32 +18,33 @@ on:
# in this file!
env:
- WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
+ SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
+ MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
CARGO_NDK_VERSION: "3.1.2"
+ SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
+ SCITER_NASM_DEBVERSION: "2.14-1"
LLVM_VERSION: "15.0.6"
- FLUTTER_VERSION: "3.19.6"
- ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem).
- FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
+ FLUTTER_VERSION: "3.24.5"
+ ANDROID_FLUTTER_VERSION: "3.24.5"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
- # vcpkg version: 2024.07.12
- VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
- VERSION: "1.2.7"
- NDK_VERSION: "r26d"
+ # vcpkg version: 2025.01.13
+ VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
+ VERSION: "1.3.7"
+ NDK_VERSION: "r27c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
- # To make a custom build with your own servers set the below secret values
- RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
- RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
- API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
+ generate-bridge:
+ uses: ./.github/workflows/bridge.yml
+
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
with:
@@ -56,8 +57,8 @@ jobs:
fail-fast: false
build-for-windows-flutter:
- name: ${{ matrix.job.target }}
- needs: [build-RustDeskTempTopMostWindow]
+ name: ${{ matrix.job.target }}
+ needs: [build-RustDeskTempTopMostWindow, generate-bridge]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
@@ -65,7 +66,12 @@ jobs:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- - { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static }
+ - {
+ target: x86_64-pc-windows-msvc,
+ os: windows-2022,
+ arch: x86_64,
+ vcpkg-triplet: x64-windows-static,
+ }
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
@@ -77,6 +83,14 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
@@ -88,12 +102,27 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: true
+
+ # https://github.com/flutter/flutter/issues/155685
+ - name: Replace engine with rustdesk custom flutter engine
+ run: |
+ flutter doctor -v
+ flutter precache --windows
+ Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
+ Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
+ mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
+
+ - name: Patch flutter
+ shell: bash
+ run: |
+ cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
- toolchain: ${{ env.WIN_RUST_VERSION }}
+ toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
@@ -101,24 +130,31 @@ jobs:
with:
prefix-key: ${{ matrix.job.os }}
- - name: Install flutter rust bridge deps
- run: |
- git config --global core.longpaths true
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- Push-Location flutter ; flutter pub get ; Pop-Location
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
-
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: |
- $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
+ if ! $VCPKG_ROOT/vcpkg \
+ install \
+ --triplet ${{ matrix.job.vcpkg-triplet }} \
+ --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Build rustdesk
@@ -158,7 +194,7 @@ jobs:
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master
with:
- name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
+ name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: rustdesk
- name: Sign rustdesk files
@@ -221,7 +257,12 @@ jobs:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- - { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static }
+ - {
+ target: i686-pc-windows-msvc,
+ os: windows-2022,
+ arch: x86,
+ vcpkg-triplet: x86-windows-static,
+ }
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps:
- name: Export GitHub Actions cache environment variables
@@ -232,7 +273,9 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@master
@@ -255,12 +298,26 @@ jobs:
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: |
- $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
+ if ! $VCPKG_ROOT/vcpkg \
+ install \
+ --triplet ${{ matrix.job.vcpkg-triplet }} \
+ --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Build rustdesk
@@ -280,7 +337,7 @@ jobs:
# Do not remove x64 files, because the user may run the 32bit version on a 64bit system.
# Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64,
# we need the x64 exe to install and uninstall the driver.
- rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat
+ rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat
mv ./usbmmidd_v2 ./Release || true
- name: find Runner.res
@@ -336,6 +393,7 @@ jobs:
# use build-for-macOS instead
if: false
runs-on: [self-hosted, macOS, ARM64]
+ needs: [generate-bridge]
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
@@ -345,14 +403,15 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- - name: Install flutter rust bridge deps
- shell: bash
- run: |
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- pushd flutter && flutter pub get && popd
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
- name: Build rustdesk
run: |
@@ -408,6 +467,7 @@ jobs:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa
runs-on: ${{ matrix.job.os }}
+ needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
@@ -430,21 +490,43 @@ jobs:
run: |
brew install nasm yasm
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
+ - name: Patch flutter
+ run: |
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
run: |
- $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
+ if ! $VCPKG_ROOT/vcpkg \
+ install \
+ --triplet ${{ matrix.job.vcpkg-triplet }} \
+ --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Install Rust toolchain
@@ -459,17 +541,22 @@ jobs:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}
- - name: Install flutter rust bridge deps
- shell: bash
- run: |
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- pushd flutter && flutter pub get && popd
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
- name: Build rustdesk lib
run: |
rustup target add ${{ matrix.job.target }}
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
+
+ - name: Upload liblibrustdesk.a Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: liblibrustdesk.a
+ path: target/aarch64-apple-ios/release/liblibrustdesk.a
- name: Build rustdesk
shell: bash
@@ -499,6 +586,7 @@ jobs:
#if: ${{ inputs.upload-artifact }}
if: false
runs-on: [self-hosted, macOS, ARM64]
+ needs: [generate-bridge]
strategy:
fail-fast: false
steps:
@@ -510,16 +598,17 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
- - name: Install flutter rust bridge deps
- shell: bash
- run: |
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- pushd flutter && flutter pub get && popd
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
- name: Build rustdesk lib
run: |
@@ -554,6 +643,7 @@ jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
+ needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
@@ -563,13 +653,15 @@ jobs:
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
+ vcpkg-triplet: x64-osx,
}
- {
target: aarch64-apple-darwin,
os: macos-latest,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
- extra-build-args: "",
+ extra-build-args: "--screencapturekit",
arch: aarch64,
+ vcpkg-triplet: arm64-osx,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -580,7 +672,9 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
@@ -617,7 +711,13 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
+ brew install llvm create-dmg nasm cmake gcc wget ninja
+ # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
+ if command -v pkg-config &>/dev/null; then
+ echo "pkg-config is already installed"
+ else
+ brew install pkg-config
+ fi
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -625,6 +725,11 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
+ - name: Patch flutter
+ run: |
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+
- name: Workaround for flutter issue
shell: bash
run: |
@@ -636,7 +741,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
- toolchain: ${{ env.RUST_VERSION }}
+ toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
@@ -644,21 +749,33 @@ jobs:
with:
prefix-key: ${{ matrix.job.os }}
- - name: Install flutter rust bridge deps
- shell: bash
- run: |
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
- pushd flutter && flutter pub get && popd
- ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
run: |
- $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
+ if ! $VCPKG_ROOT/vcpkg \
+ install \
+ --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
- name: Show version information (Rust, cargo, Clang)
shell: bash
@@ -672,6 +789,13 @@ jobs:
- name: Build rustdesk
run: |
+ if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then
+ MIN_MACOS_VERSION="12.3"
+ sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py
+ sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
+ sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
+ sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
+ fi
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
@@ -759,11 +883,8 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz
- generate-bridge-linux:
- uses: ./.github/workflows/bridge.yml
-
build-rustdesk-android:
- needs: [generate-bridge-linux]
+ needs: [generate-bridge]
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
@@ -774,15 +895,35 @@ jobs:
arch: aarch64,
target: aarch64-linux-android,
os: ubuntu-20.04,
- openssl-arch: android-arm64,
+ reltype: release,
+ suffix: "",
}
- {
arch: armv7,
target: armv7-linux-androideabi,
os: ubuntu-20.04,
- openssl-arch: android-arm,
+ reltype: release,
+ suffix: "",
+ }
+ - {
+ arch: x86_64,
+ target: x86_64-linux-android,
+ os: ubuntu-20.04,
+ reltype: release,
+ suffix: "",
}
steps:
+ - name: Free Disk Space (Ubuntu)
+ uses: jlumbroso/free-disk-space@main
+ with:
+ tool-cache: false
+ android: false
+ dotnet: true
+ haskell: true
+ large-packages: false
+ docker-images: true
+ swap-storage: false
+
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
@@ -801,7 +942,7 @@ jobs:
git \
g++ \
g++-multilib \
- libappindicator3-dev \
+ libayatana-appindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-10-dev \
@@ -811,7 +952,6 @@ jobs:
libpam0g-dev \
libpulse-dev \
libva-dev \
- libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
@@ -820,18 +960,27 @@ jobs:
llvm-10-dev \
nasm \
ninja-build \
- openjdk-11-jdk-headless \
+ openjdk-17-jdk-headless \
pkg-config \
tree \
wget
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
+
+ - name: Patch flutter
+ run: |
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
@@ -843,17 +992,34 @@ jobs:
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
run: |
case ${{ matrix.job.target }} in
aarch64-linux-android)
- ./flutter/build_android_deps.sh arm64-v8a
+ ANDROID_TARGET=arm64-v8a
;;
armv7-linux-androideabi)
- ./flutter/build_android_deps.sh armeabi-v7a
+ ANDROID_TARGET=armeabi-v7a
+ ;;
+ x86_64-linux-android)
+ ANDROID_TARGET=x86_64
+ ;;
+ i686-linux-android)
+ ANDROID_TARGET=x86
;;
esac
+ if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
shell: bash
- name: Restore bridge files
@@ -873,20 +1039,13 @@ jobs:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
- - name: fix android for flutter 3.13
- if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
- run: |
- sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
- cd flutter/lib
- find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
-
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
- cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
+ cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
@@ -898,14 +1057,30 @@ jobs:
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
;;
+ x86_64-linux-android)
+ ./flutter/ndk_x64.sh
+ mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
+ ;;
+ i686-linux-android)
+ ./flutter/ndk_x86.sh
+ mkdir -p ./flutter/android/app/src/main/jniLibs/x86
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
+ ;;
esac
+ - name: Upload Rustdesk library to Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: librustdesk.so.${{ matrix.job.target }}
+ path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
+
- name: Build rustdesk
shell: bash
env:
- JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
+ JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
- export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
+ export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
@@ -915,8 +1090,8 @@ jobs:
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter
pushd flutter
- flutter build apk --release --target-platform android-arm64 --split-per-abi
- mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
+ flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi
+ mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
@@ -924,13 +1099,39 @@ jobs:
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter
pushd flutter
- flutter build apk --release --target-platform android-arm --split-per-abi
- mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
+ flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi
+ mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
+ ;;
+ x86_64-linux-android)
+ mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
+ # build flutter
+ pushd flutter
+ flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi
+ mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
+ ;;
+ i686-linux-android)
+ mkdir -p ./flutter/android/app/src/main/jniLibs/x86
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
+ # build flutter
+ pushd flutter
+ flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi
+ mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
esac
popd
mkdir -p signed-apk; pushd signed-apk
- mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk .
+ mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
+
+ # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
+ - name: Setup sign tool version variable
+ shell: bash
+ run: |
+ BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
+ echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
+ echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
@@ -943,8 +1144,8 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
- # override default build-tools version (29.0.3) -- optional
- BUILD_TOOLS_VERSION: "30.0.2"
+ # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
+ BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -971,9 +1172,190 @@ jobs:
files: |
signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
+ build-rustdesk-android-universal:
+ needs: [build-rustdesk-android]
+ name: build rustdesk android universal apk
+ if: ${{ inputs.upload-artifact }}
+ runs-on: ubuntu-20.04
+ env:
+ reltype: release
+ x86_target: "" # can be ",android-x86"
+ suffix: ""
+ steps:
+ - name: Free Disk Space (Ubuntu)
+ uses: jlumbroso/free-disk-space@main
+ with:
+ tool-cache: false
+ android: false
+ dotnet: true
+ haskell: true
+ large-packages: false
+ docker-images: true
+ swap-storage: false
+
+ - name: Export GitHub Actions cache environment variables
+ uses: actions/github-script@v6
+ with:
+ script: |
+ core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
+ core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ clang \
+ cmake \
+ curl \
+ gcc-multilib \
+ git \
+ g++ \
+ g++-multilib \
+ libayatana-appindicator3-dev \
+ libasound2-dev \
+ libc6-dev \
+ libclang-10-dev \
+ libgstreamer1.0-dev \
+ libgstreamer-plugins-base1.0-dev \
+ libgtk-3-dev \
+ libpam0g-dev \
+ libpulse-dev \
+ libva-dev \
+ libxcb-randr0-dev \
+ libxcb-shape0-dev \
+ libxcb-xfixes0-dev \
+ libxdo-dev \
+ libxfixes-dev \
+ llvm-10-dev \
+ nasm \
+ ninja-build \
+ openjdk-17-jdk-headless \
+ pkg-config \
+ tree \
+ wget
+
+ - name: Checkout source code
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
+
+ - name: Patch flutter
+ run: |
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Download Rustdesk library from Artifacts
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk.so.aarch64-linux-android
+ path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
+
+ - name: Download Rustdesk library from Artifacts
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk.so.armv7-linux-androideabi
+ path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
+
+ - name: Download Rustdesk library from Artifacts
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk.so.x86_64-linux-android
+ path: ./flutter/android/app/src/main/jniLibs/x86_64
+
+ - name: Download Rustdesk library from Artifacts
+ if: ${{ env.reltype == 'debug' }}
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk.so.i686-linux-android
+ path: ./flutter/android/app/src/main/jniLibs/x86
+
+ - name: Build rustdesk
+ shell: bash
+ env:
+ JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
+ run: |
+ export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
+ # temporary use debug sign config
+ sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
+ mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
+ mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
+ mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
+ if [ "${{ env.reltype }}" = "debug" ]; then
+ mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
+ cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
+ fi
+ # build flutter
+ pushd flutter
+ flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }}
+ popd
+ mkdir -p signed-apk
+ mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
+
+ # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
+ - name: Setup sign tool version variable
+ shell: bash
+ run: |
+ BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
+ echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
+ echo Last build tool version is: $BUILD_TOOL_VERSION
+
+ - uses: r0adkll/sign-android-release@v1
+ name: Sign app APK
+ if: env.ANDROID_SIGNING_KEY != null
+ id: sign-rustdesk
+ with:
+ releaseDirectory: ./signed-apk
+ signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
+ alias: ${{ secrets.ANDROID_ALIAS }}
+ keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
+ keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
+ env:
+ # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
+ BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
+
+ - name: Upload Artifacts
+ if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
+ uses: actions/upload-artifact@master
+ with:
+ name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
+ path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
+
+ - name: Publish signed apk package
+ if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
+ uses: softprops/action-gh-release@v1
+ with:
+ prerelease: true
+ tag_name: ${{ env.TAG_NAME }}
+ files: |
+ ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
+
+ - name: Publish unsigned apk package
+ if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
+ uses: softprops/action-gh-release@v1
+ with:
+ prerelease: true
+ tag_name: ${{ env.TAG_NAME }}
+ files: |
+ signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
+
build-rustdesk-linux:
- needs: [generate-bridge-linux]
- name: build rustdesk linux ${{ matrix.job.target }}
+ needs: [generate-bridge]
+ name: build rustdesk linux ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
strategy:
fail-fast: false
@@ -992,7 +1374,7 @@ jobs:
arch: aarch64,
target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04,
- on: [self-hosted, Linux, ARM64],
+ on: ubuntu-22.04-arm,
deb_arch: arm64,
vcpkg-triplet: arm64-linux,
}
@@ -1005,16 +1387,20 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Maximize build space
- if: ${{ matrix.job.arch == 'x86_64' }}
run: |
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo apt-get update -y
- sudo apt-get install -y nasm qemu-user-static
+ sudo apt-get install -y nasm
+ if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then
+ sudo apt-get install -y qemu-user-static
+ fi
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }}
@@ -1058,11 +1444,26 @@ jobs:
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+ doNotCache: false
- name: Install vcpkg dependencies
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: |
- $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
+ sudo apt install -y libva-dev && apt show libva-dev
+ if ! $VCPKG_ROOT/vcpkg \
+ install \
+ --triplet ${{ matrix.job.vcpkg-triplet }} \
+ --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Restore bridge files
@@ -1098,7 +1499,7 @@ jobs:
gcc \
git \
g++ \
- libappindicator3-dev \
+ libayatana-appindicator3-dev \
libasound2-dev \
libclang-10-dev \
libgstreamer1.0-dev \
@@ -1107,7 +1508,6 @@ jobs:
libpam0g-dev \
libpulse-dev \
libva-dev \
- libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
@@ -1194,6 +1594,19 @@ jobs:
;;
esac
+ if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
+ case ${{ matrix.job.arch }} in
+ aarch64)
+ pushd /opt/flutter-elinux/flutter
+ ;;
+ x86_64)
+ pushd /opt/flutter
+ ;;
+ esac
+ git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
+ popd
+ fi
+
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
@@ -1241,16 +1654,16 @@ jobs:
rustdesk-*.deb
rustdesk-*.rpm
- - name: Upload deb
+ - name: Upload deb
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
-
+
# only x86_64 for arch since we can not find newest arm64 docker image to build
# old arch image does not make sense for arch since it is "arch" which always update to date
- # and failed to makepkg arm64 on x86_64
+ # and failed to makepkg arm64 on x86_64
- name: Patch archlinux PKGBUILD
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
run: |
@@ -1278,7 +1691,6 @@ jobs:
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
- needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
runs-on: ${{ matrix.job.on }}
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
strategy:
@@ -1299,7 +1711,7 @@ jobs:
- {
arch: armv7,
target: armv7-unknown-linux-gnueabihf,
- on: [self-hosted, Linux, ARM64],
+ on: ubuntu-22.04-arm,
distro: ubuntu18.04-rustdesk,
deb_arch: armhf,
sciter_arch: arm32,
@@ -1315,7 +1727,9 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Free Space
run: |
@@ -1325,7 +1739,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
- toolchain: ${{ env.RUST_VERSION }}
+ toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
@@ -1345,19 +1759,17 @@ jobs:
ls -l "${PWD}"
dockerRunArgs: |
--volume "${PWD}:/workspace"
- --volume "/opt/artifacts:/opt/artifacts"
shell: /bin/bash
install: |
apt-get update
apt-get install -y \
build-essential \
clang \
- cmake \
curl \
gcc \
git \
g++ \
- libappindicator3-dev \
+ libayatana-appindicator3-dev \
libasound2-dev \
libclang-dev \
libdbus-1-dev \
@@ -1369,7 +1781,6 @@ jobs:
libpam0g-dev \
libpulse-dev \
libva-dev \
- libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
@@ -1384,49 +1795,87 @@ jobs:
wget \
xz-utils \
zip
- # install newer nasm for aom
- wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_2.14-1_${{ matrix.job.deb_arch }}.deb"
- dpkg -i nasm.deb
- rm -f nasm.deb
- run: |
- # disable git safe.directory
- git config --global --add safe.directory "*"
- # Set python3.7 as default python3
- update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1
- # vcpkg
+ # arm-linux needs CMake and vcokg built from source as there
+ # are no prebuilts available from Kitware and Microsoft
+ if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
+ # install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
+ apt-get install -y gcc-8 g++-8 libssl-dev
+ # bootstrap CMake amd add it to PATH
+ git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
+ pushd /tmp/cmake
+ ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf"
+ make -j1 install
+ popd
+ rm -rf /tmp/cmake
+ export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
+ fi
+ # bootstrap vcpkg and set VCPKG_ROOT
export VCPKG_ROOT=/opt/artifacts/vcpkg
+ mkdir -p /opt/artifacts
pushd /opt/artifacts
rm -rf vcpkg
git clone https://github.com/microsoft/vcpkg
pushd vcpkg
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
- sh bootstrap-vcpkg.sh -disableMetrics
+ # build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux
+ if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
+ CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
+ else
+ sh bootstrap-vcpkg.sh -disableMetrics
+ fi
popd
popd
- # override Linux compiler detection in vcpkg
- cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
- $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
# rust
pushd /opt
# do not use rustup, because memory overflow in qemu
- wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
+ wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
- cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
+ pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
+ ./install.sh
+ popd
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
- # edit config
+ popd
+ # install newer nasm for aom
+ wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb"
+ dpkg -i nasm.deb
+ rm -f nasm.deb
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ # set python3.7 as default python3
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1
+ # add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux
+ if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
+ export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
+ export VCPKG_FORCE_SYSTEM_BINARIES=1
+ fi
+ # edit cargo config
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config
cat ~/.cargo/config
-
- # build
- pushd /workspace
+ # install dependencies from vcpkg
+ export VCPKG_ROOT=/opt/artifacts/vcpkg
+ # remove this when support higher version
+ export USE_AOM_391=1
+ if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then
+ find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
+ echo "$_1:"
+ echo "======"
+ cat "$_1"
+ echo "======"
+ echo ""
+ done
+ exit 1
+ fi
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
+ # build rustdesk
python3 ./res/inline-sciter.py
export CARGO_INCREMENTAL=0
cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
- # package
+ # make debian package
mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
@@ -1450,7 +1899,7 @@ jobs:
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- - name: Upload deb
+ - name: Upload deb
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
@@ -1466,17 +1915,13 @@ jobs:
fail-fast: false
matrix:
job:
- - {
- target: x86_64-unknown-linux-gnu,
- arch: x86_64,
- }
- - {
- target: aarch64-unknown-linux-gnu,
- arch: aarch64,
- }
+ - { target: x86_64-unknown-linux-gnu, arch: x86_64 }
+ - { target: aarch64-unknown-linux-gnu, arch: aarch64 }
steps:
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Download Binary
uses: actions/download-artifact@master
@@ -1515,7 +1960,7 @@ jobs:
build-flatpak:
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
- needs:
+ needs:
- build-rustdesk-linux
- build-rustdesk-linux-sciter
runs-on: ${{ matrix.job.on }}
@@ -1542,13 +1987,15 @@ jobs:
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
- on: [self-hosted, Linux, ARM64],
+ on: ubuntu-22.04-arm,
arch: aarch64,
suffix: "",
}
steps:
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Download Binary
uses: actions/download-artifact@master
@@ -1574,36 +2021,17 @@ jobs:
shell: /bin/bash
install: |
apt-get update -y
- apt-get install -y \
- curl \
- git \
- rpm \
- wget
+ apt-get install -y git flatpak flatpak-builder
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
- # install
- apt-get update -y
- apt-get install -y \
- cmake \
- curl \
- flatpak \
- flatpak-builder \
- gcc \
- git \
- g++ \
- libgtk-3-dev \
- nasm \
- wget
# flatpak deps
- flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
- flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
+ flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
- flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
+ flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
@@ -1624,7 +2052,9 @@ jobs:
RELEASE_NAME: web-basic
steps:
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Prepare env
run: |
@@ -1636,7 +2066,12 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: true
+
+ - name: Patch flutter
+ shell: bash
+ run: |
+ cd $(dirname $(dirname $(which flutter)))
+ [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
# https://rustdesk.com/docs/en/dev/build/web/
- name: Build web
diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml
index d788858ab4c5..7cb1f801c529 100644
--- a/.github/workflows/playground.yml
+++ b/.github/workflows/playground.yml
@@ -16,9 +16,9 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
- # vcpkg version: 2024.06.15
- VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
- VERSION: "1.2.7"
+ # vcpkg version: 2024.11.16
+ VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
+ VERSION: "1.3.7"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -90,7 +90,8 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
-
+ submodules: recursive
+
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
@@ -149,7 +150,7 @@ jobs:
shell: bash
run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
- cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
+ cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked
# below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
pushd flutter && flutter pub get && popd
@@ -250,6 +251,7 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
+ submodules: recursive
- name: Install dependencies
run: |
@@ -262,7 +264,7 @@ jobs:
git \
g++ \
g++-multilib \
- libappindicator3-dev \
+ libayatana-appindicator3-dev\
libasound2-dev \
libc6-dev \
libclang-10-dev \
@@ -302,7 +304,7 @@ jobs:
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
- cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
+ cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
pushd flutter ; flutter pub get ; popd
@@ -347,7 +349,7 @@ jobs:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
- cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
+ cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
diff --git a/.gitignore b/.gitignore
index 30e1aafe2f10..b4ea62660468 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,4 @@ examples/**/target/
vcpkg_installed
flutter/lib/generated_plugin_registrant.dart
libsciter.dylib
+flutter/web/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000000..d80e69aa84a4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "libs/hbb_common"]
+ path = libs/hbb_common
+ url = https://github.com/rustdesk/hbb_common
diff --git a/Cargo.lock b/Cargo.lock
index 936d12b01325..4b762259505d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android-wakelock"
version = "0.1.0"
-source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
+source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
dependencies = [
"jni 0.21.1",
"log",
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arboard"
version = "3.4.0"
-source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6"
+source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
dependencies = [
"clipboard-win",
"core-graphics 0.23.2",
@@ -234,24 +234,13 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"parking_lot",
- "resvg",
+ "serde 1.0.203",
+ "serde_derive",
"windows-sys 0.48.0",
"wl-clipboard-rs",
"x11rb 0.13.1",
]
-[[package]]
-name = "arrayref"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
-
-[[package]]
-name = "arrayvec"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
-
[[package]]
name = "async-broadcast"
version = "0.5.1"
@@ -734,9 +723,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
-version = "1.16.1"
+version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
@@ -746,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.6.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
dependencies = [
"serde 1.0.203",
]
@@ -871,6 +860,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "chrono"
version = "0.4.38"
@@ -898,6 +893,20 @@ dependencies = [
"regex",
]
+[[package]]
+name = "cidre"
+version = "0.4.0"
+source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
+dependencies = [
+ "cidre-macros",
+ "parking_lot",
+]
+
+[[package]]
+name = "cidre-macros"
+version = "0.1.0"
+source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
+
[[package]]
name = "cipher"
version = "0.4.4"
@@ -987,11 +996,14 @@ dependencies = [
[[package]]
name = "clipboard-master"
version = "4.0.0-beta.6"
-source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c"
+source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695"
dependencies = [
"objc",
"objc-foundation",
"objc_id",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
"windows-win",
"wl-clipboard-rs",
"x11-clipboard 0.9.2",
@@ -1000,9 +1012,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
-version = "5.3.1"
+version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
+checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
dependencies = [
"error-code",
]
@@ -1278,10 +1290,10 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
+source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a"
dependencies = [
"alsa",
+ "cidre",
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
"coreaudio-rs",
"dasp_sample",
@@ -1526,12 +1538,6 @@ dependencies = [
"dasp_sample",
]
-[[package]]
-name = "data-url"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
-
[[package]]
name = "dbus"
version = "0.9.7"
@@ -1575,6 +1581,16 @@ dependencies = [
"windows 0.32.0",
]
+[[package]]
+name = "default_net"
+version = "0.1.0"
+source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e"
+dependencies = [
+ "anyhow",
+ "regex",
+ "winapi 0.3.9",
+]
+
[[package]]
name = "deranged"
version = "0.3.11"
@@ -2081,12 +2097,6 @@ dependencies = [
"thiserror",
]
-[[package]]
-name = "float-cmp"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
-
[[package]]
name = "flume"
version = "0.11.0"
@@ -2143,29 +2153,6 @@ dependencies = [
"libm",
]
-[[package]]
-name = "fontconfig-parser"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d"
-dependencies = [
- "roxmltree 0.19.0",
-]
-
-[[package]]
-name = "fontdb"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
-dependencies = [
- "fontconfig-parser",
- "log",
- "memmap2",
- "slotmap",
- "tinyvec",
- "ttf-parser",
-]
-
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -2274,9 +2261,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@@ -2284,9 +2271,9 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
@@ -2301,9 +2288,9 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
@@ -2335,9 +2322,9 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2 1.0.86",
"quote 1.0.36",
@@ -2346,21 +2333,21 @@ dependencies = [
[[package]]
name = "futures-sink"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@@ -2924,6 +2911,7 @@ dependencies = [
"bytes",
"chrono",
"confy",
+ "default_net",
"directories-next",
"dirs-next",
"dlopen",
@@ -2948,6 +2936,7 @@ dependencies = [
"serde 1.0.203",
"serde_derive",
"serde_json 1.0.118",
+ "sha2",
"socket2 0.3.19",
"sodiumoxide",
"sysinfo",
@@ -3087,8 +3076,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
-version = "0.5.1"
-source = "git+https://github.com/21pages/hwcodec#74e8288f776a9d43861f16aa62e86b57c7209868"
+version = "0.7.1"
+source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -3213,16 +3202,10 @@ dependencies = [
"tiff",
]
-[[package]]
-name = "imagesize"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
-
[[package]]
name = "impersonate_system"
version = "0.1.0"
-source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
+source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
dependencies = [
"cc",
]
@@ -3462,16 +3445,6 @@ dependencies = [
"unicode-segmentation",
]
-[[package]]
-name = "kurbo"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c"
-dependencies = [
- "arrayvec",
- "smallvec",
-]
-
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -3733,7 +3706,7 @@ dependencies = [
[[package]]
name = "machine-uid"
version = "0.3.0"
-source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
+source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -3777,15 +3750,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
-[[package]]
-name = "memmap2"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
-dependencies = [
- "libc",
-]
-
[[package]]
name = "memoffset"
version = "0.6.5"
@@ -3847,14 +3811,6 @@ dependencies = [
"windows-sys 0.48.0",
]
-[[package]]
-name = "mouce"
-version = "0.2.1"
-source = "git+https://github.com/rustdesk-org/mouce.git#177625a395cd8fa73964714d0039535cb9b47893"
-dependencies = [
- "glob",
-]
-
[[package]]
name = "muda"
version = "0.13.5"
@@ -4043,11 +3999,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
- "cfg_aliases",
+ "cfg_aliases 0.1.1",
"libc",
"memoffset 0.9.1",
]
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if 1.0.0",
+ "cfg_aliases 0.2.1",
+ "libc",
+]
+
[[package]]
name = "nom"
version = "7.1.3"
@@ -4207,7 +4175,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
- "proc-macro-crate 2.0.2",
+ "proc-macro-crate 1.3.1",
"proc-macro2 1.0.86",
"quote 1.0.36",
"syn 2.0.68",
@@ -4426,9 +4394,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
-version = "0.10.64"
+version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
@@ -4458,9 +4426,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
-version = "0.9.102"
+version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
@@ -4732,15 +4700,9 @@ version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
dependencies = [
- "siphasher 0.2.3",
+ "siphasher",
]
-[[package]]
-name = "pico-args"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
-
[[package]]
name = "pin-project"
version = "1.1.5"
@@ -5065,6 +5027,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quick-xml"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quote"
version = "0.6.13"
@@ -5260,7 +5231,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
-source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488"
+source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
dependencies = [
"cocoa 0.24.1",
"core-foundation 0.9.4",
@@ -5414,31 +5385,6 @@ dependencies = [
"winreg 0.50.0",
]
-[[package]]
-name = "resvg"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
-dependencies = [
- "gif",
- "jpeg-decoder",
- "log",
- "pico-args",
- "rgb",
- "svgtypes",
- "tiny-skia",
- "usvg",
-]
-
-[[package]]
-name = "rgb"
-version = "0.8.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741"
-dependencies = [
- "bytemuck",
-]
-
[[package]]
name = "ring"
version = "0.17.8"
@@ -5463,18 +5409,6 @@ dependencies = [
"crossbeam-utils",
]
-[[package]]
-name = "roxmltree"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
-
-[[package]]
-name = "roxmltree"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
-
[[package]]
name = "rpassword"
version = "2.1.0"
@@ -5544,7 +5478,7 @@ dependencies = [
[[package]]
name = "rust-pulsectl"
version = "0.2.12"
-source = "git+https://github.com/open-trade/pulsectl#5e68f4c2b7c644fa321984688602d71e8ad0bba3"
+source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2"
dependencies = [
"libpulse-binding",
]
@@ -5572,7 +5506,7 @@ dependencies = [
[[package]]
name = "rustdesk"
-version = "1.2.7"
+version = "1.3.7"
dependencies = [
"android-wakelock",
"android_logger",
@@ -5604,6 +5538,7 @@ dependencies = [
"flutter_rust_bridge",
"fon",
"fruitbasket",
+ "gtk",
"hbb_common",
"hex",
"hound",
@@ -5618,7 +5553,7 @@ dependencies = [
"libpulse-simple-binding",
"mac_address",
"magnum-opus",
- "mouce",
+ "nix 0.29.0",
"num_cpus",
"objc",
"objc_id",
@@ -5650,6 +5585,7 @@ dependencies = [
"system_shutdown",
"tao",
"tauri-winrt-notification",
+ "termios",
"totp-rs",
"tray-icon",
"url",
@@ -5670,7 +5606,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
-version = "1.2.7"
+version = "1.3.7"
dependencies = [
"brotli",
"dirs 5.0.1",
@@ -5853,22 +5789,6 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
-[[package]]
-name = "rustybuzz"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
-dependencies = [
- "bitflags 2.6.0",
- "bytemuck",
- "smallvec",
- "ttf-parser",
- "unicode-bidi-mirroring",
- "unicode-ccc",
- "unicode-properties",
- "unicode-script",
-]
-
[[package]]
name = "ryu"
version = "1.0.18"
@@ -5905,7 +5825,7 @@ dependencies = [
[[package]]
name = "sciter-rs"
version = "0.5.57"
-source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#fab913b7c2e779b05c249b0c5de5a08759b2c15d"
+source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821"
dependencies = [
"lazy_static",
"libc",
@@ -6159,27 +6079,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
-[[package]]
-name = "simplecss"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
-dependencies = [
- "log",
-]
-
[[package]]
name = "siphasher"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
-[[package]]
-name = "siphasher"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
-
[[package]]
name = "slab"
version = "0.4.9"
@@ -6189,15 +6094,6 @@ dependencies = [
"autocfg 1.3.0",
]
-[[package]]
-name = "slotmap"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
-dependencies = [
- "version_check",
-]
-
[[package]]
name = "smallvec"
version = "1.13.2"
@@ -6268,15 +6164,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
-[[package]]
-name = "strict-num"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
-dependencies = [
- "float-cmp",
-]
-
[[package]]
name = "strsim"
version = "0.8.0"
@@ -6338,16 +6225,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-[[package]]
-name = "svgtypes"
-version = "0.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c"
-dependencies = [
- "kurbo",
- "siphasher 1.0.1",
-]
-
[[package]]
name = "syn"
version = "0.15.44"
@@ -6399,7 +6276,7 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.29.10"
-source = "git+https://github.com/rustdesk-org/sysinfo#f45dcc6510d48c3a1401c5a33eedccc8899f67b2"
+source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -6593,11 +6470,11 @@ dependencies = [
[[package]]
name = "tfc"
-version = "0.6.1"
-source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342"
+version = "0.7.0"
+source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0"
dependencies = [
"anyhow",
- "core-graphics 0.22.3",
+ "core-graphics 0.23.2",
"unicode-segmentation",
"winapi 0.3.9",
"x11 2.19.0",
@@ -6687,32 +6564,6 @@ dependencies = [
"time-core",
]
-[[package]]
-name = "tiny-skia"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
-dependencies = [
- "arrayref",
- "arrayvec",
- "bytemuck",
- "cfg-if 1.0.0",
- "log",
- "png",
- "tiny-skia-path",
-]
-
-[[package]]
-name = "tiny-skia-path"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
-dependencies = [
- "arrayref",
- "bytemuck",
- "strict-num",
-]
-
[[package]]
name = "tinyvec"
version = "1.6.1"
@@ -7004,12 +6855,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-[[package]]
-name = "ttf-parser"
-version = "0.21.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
-
[[package]]
name = "typenum"
version = "1.17.0"
@@ -7082,18 +6927,6 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
-[[package]]
-name = "unicode-bidi-mirroring"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
-
-[[package]]
-name = "unicode-ccc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
-
[[package]]
name = "unicode-ident"
version = "1.0.12"
@@ -7109,30 +6942,12 @@ dependencies = [
"tinyvec",
]
-[[package]]
-name = "unicode-properties"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
-
-[[package]]
-name = "unicode-script"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
-
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
-[[package]]
-name = "unicode-vo"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
-
[[package]]
name = "unicode-width"
version = "0.1.13"
@@ -7195,33 +7010,6 @@ dependencies = [
"log",
]
-[[package]]
-name = "usvg"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
-dependencies = [
- "base64 0.22.1",
- "data-url",
- "flate2",
- "fontdb",
- "imagesize",
- "kurbo",
- "log",
- "pico-args",
- "roxmltree 0.20.0",
- "rustybuzz",
- "simplecss",
- "siphasher 1.0.1",
- "strict-num",
- "svgtypes",
- "tiny-skia-path",
- "unicode-bidi",
- "unicode-script",
- "unicode-vo",
- "xmlwriter",
-]
-
[[package]]
name = "utf16string"
version = "0.2.0"
@@ -7309,7 +7097,7 @@ dependencies = [
[[package]]
name = "wallpaper"
version = "3.2.0"
-source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
+source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
dependencies = [
"dirs 5.0.1",
"enquote",
@@ -7414,9 +7202,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "wayland-backend"
-version = "0.3.4"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07"
+checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
dependencies = [
"cc",
"downcast-rs",
@@ -7428,9 +7216,9 @@ dependencies = [
[[package]]
name = "wayland-client"
-version = "0.31.3"
+version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133"
+checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
dependencies = [
"bitflags 2.6.0",
"rustix 0.38.34",
@@ -7440,9 +7228,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
-version = "0.32.1"
+version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c"
+checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
@@ -7452,9 +7240,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-wlr"
-version = "0.3.1"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24"
+checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
@@ -7465,20 +7253,20 @@ dependencies = [
[[package]]
name = "wayland-scanner"
-version = "0.31.2"
+version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565"
+checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
dependencies = [
"proc-macro2 1.0.86",
- "quick-xml 0.31.0",
+ "quick-xml 0.34.0",
"quote 1.0.36",
]
[[package]]
name = "wayland-sys"
-version = "0.31.2"
+version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12"
+checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
dependencies = [
"dlib",
"log",
@@ -7498,7 +7286,7 @@ dependencies = [
[[package]]
name = "webm"
version = "1.1.0"
-source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
+source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
dependencies = [
"webm-sys",
]
@@ -7506,7 +7294,7 @@ dependencies = [
[[package]]
name = "webm-sys"
version = "1.0.4"
-source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
+source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
dependencies = [
"cc",
]
@@ -8220,12 +8008,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "xmlwriter"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
-
[[package]]
name = "zbus"
version = "3.15.2"
diff --git a/Cargo.toml b/Cargo.toml
index 85b2b540121d..917b49a879c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
-version = "1.2.7"
+version = "1.3.7"
authors = ["rustdesk "]
edition = "2021"
build= "build.rs"
@@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
name = "naming"
path = "src/naming.rs"
+[[bin]]
+name = "service"
+path = "src/service.rs"
+
[features]
inline = []
cli = []
@@ -36,6 +40,7 @@ unix-file-copy-paste = [
"dep:once_cell",
"clipboard/unix-file-copy-paste",
]
+screencapturekit = ["cpal/screencapturekit"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -78,19 +83,20 @@ zip = "0.6"
shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
-[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
-cpal = "0.15"
+[target.'cfg(not(target_os = "linux"))'.dependencies]
+# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
+cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
ringbuf = "0.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
-sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
+sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" }
sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
ctrlc = "3.2"
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
-arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] }
+arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0"
@@ -114,7 +120,7 @@ winapi = { version = "0.3", features = [
winreg = "0.11"
windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" }
-impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
+impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12"
tauri-winrt-notification = "0.1.2"
runas = "1.2"
@@ -138,7 +144,7 @@ image = "0.24"
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
-wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
+wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
@@ -150,9 +156,8 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" }
-rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
+rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" }
async-process = "1.7"
-mouce = { git="https://github.com/rustdesk-org/mouce.git" }
evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
@@ -162,11 +167,14 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true}
+nix = { version = "0.29", features = ["term", "process"]}
+gtk = "0.18"
+termios = "0.3"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13"
jni = "0.21"
-android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
+android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
diff --git a/Dockerfile b/Dockerfile
index 8544219c2b1c..f0e4e4a4a622 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
WORKDIR /
ARG DEBIAN_FRONTEND=noninteractive
+ENV VCPKG_FORCE_SYSTEM_BINARIES=1
RUN apt update -y && \
apt install --yes --no-install-recommends \
g++ \
@@ -21,7 +22,8 @@ RUN apt update -y && \
libpam0g-dev \
libpulse-dev \
make \
- cmake \
+ wget \
+ libssl-dev \
unzip \
zip \
sudo \
@@ -31,6 +33,13 @@ RUN apt update -y && \
ninja-build && \
rm -rf /var/lib/apt/lists/*
+RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \
+ tar xzf cmake-3.30.6.tar.gz && \
+ cd cmake-3.30.6 && \
+ ./configure --prefix=/usr/local && \
+ make && \
+ make install
+
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
diff --git a/README.md b/README.md
index df8c1ca3483a..1c4d6be4afe9 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,10 @@
- Servers •
Build •
Docker •
Structure •
Snapshot
- [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ] | [Türkçe ]
+ [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ] | [Türkçe ] | [Norsk ]
We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language
@@ -25,9 +24,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
-[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[ ](https://flathub.org/apps/com.rustdesk.RustDesk)
## Dependencies
@@ -59,19 +61,19 @@ Please download Sciter dynamic library yourself.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
- libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
-sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
-sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -171,3 +173,4 @@ Please ensure that you are running these commands from the root of the RustDesk
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
+
diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml
index 411d7bb571d9..a5ae4ef78c9d 100644
--- a/appimage/AppImageBuilder-aarch64.yml
+++ b/appimage/AppImageBuilder-aarch64.yml
@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
- version: 1.2.7
- exec: usr/lib/rustdesk/rustdesk
+ version: 1.3.7
+ exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -47,9 +47,9 @@ AppDir:
- libasound2
- libsystemd0
- curl
+ - libva2
- libva-drm2
- libva-x11-2
- - libvdpau1
- libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
@@ -77,7 +77,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11
- APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
+ APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test:
diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml
index 9c6860dd43e6..25889d5b7d0c 100644
--- a/appimage/AppImageBuilder-x86_64.yml
+++ b/appimage/AppImageBuilder-x86_64.yml
@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
- version: 1.2.7
- exec: usr/lib/rustdesk/rustdesk
+ version: 1.3.7
+ exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -37,6 +37,9 @@ AppDir:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse
include:
+ # https://github.com/rustdesk/rustdesk/issues/9103
+ # Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
+ # We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
- libc6:amd64
- libgtk-3-0
- libxcb-randr0
@@ -47,9 +50,9 @@ AppDir:
- libasound2
- libsystemd0
- curl
+ - libva2
- libva-drm2
- libva-x11-2
- - libvdpau1
- libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
@@ -77,7 +80,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11
- APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
+ APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
test:
diff --git a/build.py b/build.py
index 97ec9dfe9f9e..87c0dbd3432d 100755
--- a/build.py
+++ b/build.py
@@ -9,6 +9,7 @@
import hashlib
import argparse
import sys
+from pathlib import Path
windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith(
@@ -31,6 +32,11 @@ def get_deb_arch() -> str:
return "amd64"
return custom_arch
+def get_deb_extra_depends() -> str:
+ custom_arch = os.environ.get("DEB_ARCH")
+ if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
+ return ", libatomic1"
+ return ""
def system2(cmd):
exit_code = os.system(cmd)
@@ -106,7 +112,7 @@ def make_parser():
'--hwcodec',
action='store_true',
help='Enable feature hwcodec' + (
- '' if windows or osx else ', need libva-dev, libvdpau-dev.')
+ '' if windows or osx else ', need libva-dev.')
)
parser.add_argument(
'--vram',
@@ -138,6 +144,12 @@ def make_parser():
"--package",
type=str
)
+ if osx:
+ parser.add_argument(
+ '--screencapturekit',
+ action='store_true',
+ help='Enable feature screencapturekit'
+ )
return parser
@@ -269,6 +281,9 @@ def get_features(args):
features.append('flutter')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
+ if osx:
+ if args.screencapturekit:
+ features.append('screencapturekit')
print("features:", features)
return features
@@ -278,14 +293,17 @@ def generate_control_file(version):
system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk
+Section: net
+Priority: optional
Version: %s
Architecture: %s
Maintainer: rustdesk
Homepage: https://rustdesk.com
-Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
+Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
+Recommends: libayatana-appindicator3-1
Description: A remote control software.
-""" % (version, get_deb_arch())
+""" % (version, get_deb_arch(), get_deb_extra_depends())
file = open(control_file_path, "w")
file.write(content)
file.close()
@@ -304,7 +322,7 @@ def build_flutter_deb(version, features):
os.chdir('flutter')
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/etc/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
@@ -314,7 +332,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
- f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/')
+ f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -325,8 +343,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
- system2(
- 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2(
@@ -339,7 +355,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
@@ -351,7 +367,7 @@ def build_flutter_deb(version, features):
def build_deb_from_folder(version, binary_folder):
os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
@@ -359,7 +375,7 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
- f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/')
+ f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -370,15 +386,13 @@ def build_deb_from_folder(version, binary_folder):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
- system2(
- 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
@@ -391,12 +405,13 @@ def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(
- f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
+ f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
system2('flutter build macos --release')
+ system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
@@ -608,21 +623,24 @@ def main():
os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk')
- system2('mkdir -p tmpdeb/usr/lib/rustdesk')
- system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
- system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
- md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
- md5_file('etc/rustdesk/startwm.sh')
- md5_file('etc/X11/rustdesk/xorg.conf')
- md5_file('etc/pam.d/rustdesk')
- md5_file('usr/lib/rustdesk/libsciter-gtk.so')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk')
+ system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
+ system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
+ md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
- system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
+ system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
+
+def md5_file_folder(base_dir):
+ base_path = Path(base_dir)
+ for file in base_path.rglob('*'):
+ if file.is_file() and 'DEBIAN' not in file.parts:
+ relative_path = file.relative_to(base_path)
+ md5_file(str(relative_path))
if __name__ == "__main__":
diff --git a/build.rs b/build.rs
index d332a43a22eb..3d19ee037f6f 100644
--- a/build.rs
+++ b/build.rs
@@ -1,7 +1,7 @@
#[cfg(windows)]
fn build_windows() {
let file = "src/platform/windows.cc";
- let file2 = "src/platform/windows_delete_test_cert.cc";
+ let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file);
@@ -61,7 +61,11 @@ fn install_android_deps() {
let target = format!("{}-android", target_arch);
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: std::path::PathBuf = vcpkg_root.into();
- path.push("installed");
+ if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
+ path = vcpkg_root.into();
+ } else {
+ path.push("installed");
+ }
path.push(target);
println!(
"{}",
@@ -72,7 +76,6 @@ fn install_android_deps() {
);
println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe");
- println!("cargo:rustc-link-lib=oboe_wrapper");
println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES");
}
diff --git a/docs/CODE_OF_CONDUCT-NO.md b/docs/CODE_OF_CONDUCT-NO.md
new file mode 100644
index 000000000000..baefda0519ac
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT-NO.md
@@ -0,0 +1,125 @@
+
+# Atferdskodeks for bidragsyterpaktern
+
+## Hva Vi Står For
+
+Vi som medlemer, bidragere, og ledere står for å skape ett hat-fritt felleskap,
+uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger,
+etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivå, utdanning,
+sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual
+identitet og orientasjon.
+
+Vi står for åpen, velkommende, mangfold, inklusiv og sunn oppførsel i vårt felleskap.
+
+## Våre Standarer
+
+Eksempler på oppførsel som hjelper ett positivt felleskap inkluderer:
+
+* Vise empati og vennlighet mot andre mennesker
+* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer
+* Gi og ta konstruktiv kritikk i beste mening
+* Akseptere ansvar og unskylde seg for de som er utsatt av våre feil,
+ og lære av disse
+* Fokusere på det som er best ikke bare for individer, men for felleskapet
+
+Eksempler på uakseptabel oppførsel inkluderer:
+
+* Bruk av seksualisert språk eller bilder, og seksual oppmerksomhet.
+* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep
+* Offentlig eller privat trakassering
+* Publisering av andres private informasjon, sånn som bosteds- og epost-addresser,
+ uten deres godskjenning.
+* Andre rettningslinjer som kan bli sett på som upassende i en profesjonell setting.
+
+## Håndhevingsansvar
+
+Felleskapets ledere har ansvar for å klarifisere og håndheve våre standarer av
+akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons på
+oppførsel de anser som upassende, truende, fornermende eller skadelig.
+
+Felleskapets ledere har retten og ansvaret til å fjerne, redigere, eller avslå
+kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke
+samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for
+moderatorenes valg når passende.
+
+## Omfang
+
+Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og
+de gjelder også når ett individ representerer felleskapet på offentlige medier.
+Eksempler på representasjon av vårt felleskap inkluderer bruke av offisielle e-mail
+addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en
+utpekt representant på digitale og fysiske arrangsjemanger.
+
+## Håndheving
+
+Hendelser av misbruk, trakasserende eller på noen måte uakseptert oppførsel kann
+bli raportert til felleskapets ledere med ansvar for håndheving på
+[info@rustdesk.com](mailto:info@rustdesk.com).
+All tilbakemelding vill bli sett gjennom og investigert rettferdig så fort som mulig.
+
+Alle felleskapets ledere er obligert til å respektere privatlivet og sikkerhetet ovenfor
+den som raporterer en hendelse.
+
+## Håndhevings Guide
+
+Felleskapets ledere vill følge disse Rettningslinjene for sammfunspåvirkning med
+tanke på konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene:
+
+### 1. Korreksjon
+
+**Sammfunspåvirkning**: Bruk av upassende språk eller annen oppførsel ansett som
+uprofesjonelt eller uvelkommen i dette felleskapet.
+
+**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som
+klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig
+unskyldning kan bli forespurt.
+
+### 2. Advarsel
+
+**Sammfunspåvirkning**: Ett brudd på en singulær hendelse eller en serie handlinger.
+
+**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen
+interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
+de som håndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode.
+Dette inkluderer å unngå interaksjoner i felleskapets platformer, samt eksterne
+kanaler, som f.eks sosial media. Brudd av disse vilkårene kan føre til midlertidig
+eller permanent bannlysning.
+
+### 3. Midlertidig Bannlysning
+
+**Sammfunspåvirkning**: Ett særiøst brudd på felleskapets standarer, inkludert
+vedvarende upassende oppførsel.
+
+**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller
+offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen
+interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
+de som håndhever disse etiske rettningslinjene, er tillat for denne perioden.
+Brudd på disse vilkårene kan føre til permanent bannlysning.
+
+### 4. Permanent Bannlysning
+
+**Sammfunspåvirkning**: Demonstasjon av mønster i brudd på felleskapets standarer,
+inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller
+aggresjon mot eller nedsettelse av grupper individer.
+
+**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i
+felleskapet
+
+## Attribusjon
+
+Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage],
+versjon 2.0, tilgjengelig ved
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
+
+Sammfunspåvirknings guid inspirert av
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For svar til vanlige spørsmål angående disse etiske rettningslinjene, se FAQ på
+[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig
+ved [https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/docs/CODE_OF_CONDUCT-ZH.md b/docs/CODE_OF_CONDUCT-ZH.md
new file mode 100644
index 000000000000..0877ab20f246
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT-ZH.md
@@ -0,0 +1,87 @@
+
+# 贡献者公约行为准则
+
+## 我们的承诺
+
+身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
+
+我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
+
+## 我们的标准
+
+有助于为我们的社区创造积极环境的行为例子包括但不限于:
+
+* 表现出对他人的同情和善意
+* 尊重不同的主张、观点和感受
+* 提出和大方接受建设性意见
+* 承担责任并向受我们错误影响的人道歉
+* 注重社区共同诉求,而非个人得失
+
+不当行为例子包括:
+
+* 使用情色化的语言或图像,及性引诱或挑逗
+* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
+* 公开或私下的骚扰行为
+* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
+* 其他有理由认定为违反职业操守的不当行为
+
+## 责任和权力
+
+社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
+
+社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。
+
+## 适用范围
+
+本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
+
+代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
+
+## 监督
+
+辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
+
+所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
+
+## 处理方针
+
+社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
+
+### 1. 纠正
+
+**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
+
+**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
+
+### 2. 警告
+
+**社区影响**: 单个或一系列违规行为。
+
+**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
+
+### 3. 临时封禁
+
+**社区影响**: 严重违反社区准则,包括持续的不当行为。
+
+**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
+
+### 4. 永久封禁
+
+**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
+
+**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
+
+## 参见
+
+本行为准则改编自[参与者公约][homepage]2.0 版, 参见
+[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
+
+指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
+
+有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
+
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
\ No newline at end of file
diff --git a/docs/CONTRIBUTING-NO.md b/docs/CONTRIBUTING-NO.md
new file mode 100644
index 000000000000..89a574563275
--- /dev/null
+++ b/docs/CONTRIBUTING-NO.md
@@ -0,0 +1,46 @@
+# Bidrag til RustDesk
+
+RustDesk er åpene for bidrag fra alle. Her er reglene for de som har lyst til å
+hjelpe oss:
+
+## Bidrag
+
+Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests.
+Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til
+å godkjenne endringene) og enten bli sendt til main treet eller respondert med
+tilbakemelding på endringer som er nødvendig. Alle bidrag burde følge dette formate
+også de fra kjerne bidragere.
+
+Om du ønsker å jobbe på en issue må du huske å gjøre krav på den først. Dette
+kann gjøres ved å kommentere på den GitHub issue-en du ønsker å jobbe på.
+Dette er for å hindre duplikat innsats på samme problem.
+
+## Pull Request Sjekkliste
+
+- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nåværende
+ master grenen før du sender inn din pull request. Hvis ikke dette gjøres på rent
+ vis vill du bli spurt om å rebase dine endringer.
+
+- Commits burde være så små som mulig, samtidig som de må være korrekt uavhenging av hverandre
+ (hver commit burde kompilere og bestå tester).
+
+- Commits burde være akkopaniert med en Developer Certificate of Origin
+ (http://developercertificate.org), som indikerer att du (og din arbeidsgiver
+ i det tilfellet) godkjenner å bli knyttet til vilkårene av [prosjekt lisensen](../LICENCE).
+ Ved bruk av git er dette `-s` opsjonen til `git commit`.
+
+- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til
+ å se på dem kan du @-svare en med autoritet til å godkjenne dine endringer.
+ Dette kann gjøres i en pull request, en kommentar eller via epost på [email](mailto:info@rustdesk.com).
+
+- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet.
+
+For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## Oppførsel
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
+
+## Kommunikasjon
+
+RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING-ZH.md b/docs/CONTRIBUTING-ZH.md
new file mode 100644
index 000000000000..718cdac69bb5
--- /dev/null
+++ b/docs/CONTRIBUTING-ZH.md
@@ -0,0 +1,32 @@
+# 为RustDesk做贡献
+
+Rust欢迎每一位贡献者,如果您有意向为我们做出贡献,请遵循以下指南:
+
+## 贡献方式
+
+对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
+
+如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
+
+## PR 注意事项
+
+- 从 master 分支创建一个新的分支,并在提交PR之前,如果需要,将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
+
+- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
+
+- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
+
+- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
+
+- 请为修复的 bug 或新增的功能添加相应的测试用例。
+
+有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## 行为准则
+
+请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
+
+
+## 沟通渠道
+
+RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。
diff --git a/docs/DEVCONTAINER-NO.md b/docs/DEVCONTAINER-NO.md
new file mode 100644
index 000000000000..1d944ed5d6c6
--- /dev/null
+++ b/docs/DEVCONTAINER-NO.md
@@ -0,0 +1,14 @@
+
+Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget.
+
+Nå tilbyr devcontainer linux og android builds i både debug og release modus.
+
+Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds.
+
+Kommando|Build Type|Modus
+-|-|-|
+`.devcontainer/build.sh --debug linux`|Linux|debug
+`.devcontainer/build.sh --release linux`|Linux|release
+`.devcontainer/build.sh --debug android`|android-arm64|debug
+`.devcontainer/build.sh --release android`|android-arm64|release
+
diff --git a/docs/README-KR.md b/docs/README-KR.md
index af3c665244f3..b0a8b973e2ad 100644
--- a/docs/README-KR.md
+++ b/docs/README-KR.md
@@ -9,12 +9,12 @@
README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.
-Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
-Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [스스로 설정](https://rustdesk.com/server)하는 것도, [스스로 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
+Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -43,9 +43,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
-- run `cargo run`
+- 실행 `cargo run`
-## [Build](https://rustdesk.com/docs/en/dev/build/)
+## [빌드](https://rustdesk.com/docs/en/dev/build/)
## Linux에서 빌드 순서
@@ -67,7 +67,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
-### Install vcpkg
+### vcpkg 설치
```sh
git clone https://github.com/microsoft/vcpkg
@@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
-### Fix libvpx (For Fedora)
+### libvpx 수정 (For Fedora용)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
-### Build
+### 빌드
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
## Docker에 빌드하는 방법
-레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
+리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -115,13 +115,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다.
+이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다.
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
-첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 커맨드에 다른 인수를 지정할 필요가 있다면, 커맨드 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 커맨드 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 커맨드로 실행할 수 있습니다.
+첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다.
```sh
target/debug/rustdesk
@@ -133,9 +133,9 @@ target/debug/rustdesk
target/release/rustdesk
```
-커맨드를 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 서브커맨드는 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
+명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
-## File Structure
+## 파일 구조
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, tcp/udp 랩퍼, protobuf, 파일 전송을 위한 fs 함수, 그 외 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
@@ -143,12 +143,12 @@ target/release/rustdesk
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 접속 시작
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트(TCP hole punching) 혹은 relayed 접속
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트 (TCP hole punching) 혹은 relayed 접속
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드
-- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
-- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트
-## Snapshot
+## 스냅샷
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
diff --git a/docs/README-NO.md b/docs/README-NO.md
new file mode 100644
index 000000000000..dbe4ddd2deb5
--- /dev/null
+++ b/docs/README-NO.md
@@ -0,0 +1,177 @@
+
+
+ Servere •
+ Build •
+ Docker •
+ Struktur •
+ Snapshot
+ [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ] | [Türkçe ] | [Norsk
+ Vi trenger din hjelp til å oversette denne README-en, RustDesk UI og RustDesk Doc tid ditt morsmål
+
+
+Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
+
+Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo).
+
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
+
+RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart.
+
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+[ ](https://flathub.org/apps/com.rustdesk.RustDesk)
+
+## Avhengigheter
+
+Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen.
+
+Venligst last ned Sciters dynamiske bibliotek selv.
+
+[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
+[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+
+## Rå steg for bygging
+
+- Klargjør ditt Rust development env og C++ build env
+
+- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen
+
+ - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
+ - Linux/macOS: vcpkg install libvpx libyuv opus aom
+
+- Kjør `cargo run`
+
+## [Bygg](https://rustdesk.com/docs/en/dev/build/)
+
+## Hvordan Bygge til Linux
+
+### Ubuntu 18 (Debian 10)
+
+```sh
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
+```
+
+### Fedora 28 (CentOS 8)
+
+```sh
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
+```
+
+### Arch (Manjaro)
+
+```sh
+sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
+```
+
+### Installer vcpkg
+
+```sh
+git clone https://github.com/microsoft/vcpkg
+cd vcpkg
+git checkout 2023.04.15
+cd ..
+vcpkg/bootstrap-vcpkg.sh
+export VCPKG_ROOT=$HOME/vcpkg
+vcpkg/vcpkg install libvpx libyuv opus aom
+```
+
+### Fiks libvpx (For Fedora)
+
+```sh
+cd vcpkg/buildtrees/libvpx/src
+cd *
+./configure
+sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
+sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
+make
+cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
+cd
+```
+
+### Bygg
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source $HOME/.cargo/env
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+mkdir -p target/debug
+wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+mv libsciter-gtk.so target/debug
+VCPKG_ROOT=$HOME/vcpkg cargo run
+```
+
+## Hvordan bygge med Docker
+
+Start med å klone repositoret og bygg Docker konteineren:
+
+```sh
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+docker build -t "rustdesk-builder" .
+```
+
+Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando:
+
+```sh
+docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
+```
+
+Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med:
+
+```sh
+target/debug/rustdesk
+```
+
+Eller, hvis du kjører ett release program:
+
+```sh
+target/release/rustdesk
+```
+
+Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten.
+
+## Fil Struktur
+
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll
+- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS.
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet)
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
+
+## Skjermbilder
+
+![Tilkoblings Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
+
+![Koble til Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
+
+![Fil Overføring](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
+
+![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
+
diff --git a/docs/README-PL.md b/docs/README-PL.md
index 4d3464d41ae6..ef8f42648c05 100644
--- a/docs/README-PL.md
+++ b/docs/README-PL.md
@@ -164,3 +164,4 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
+
diff --git a/docs/README-UA.md b/docs/README-UA.md
index c4d2e6f9f390..98f19d4e69d7 100644
--- a/docs/README-UA.md
+++ b/docs/README-UA.md
@@ -1,20 +1,18 @@
- Сервери •
+ Сервери •
Збирання •
Docker •
Структура •
- Знімки
- [English ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ] | [Türkçe ]
- Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації RustDesk на вашу рідну мову
+ Знімки екрана
+ [English ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ] | [Türkçe ]
+ Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації RustDesk вашою рідною мовою
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
-[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
-
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
- libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
-sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
-sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -158,18 +156,19 @@ target/release/rustdesk
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
-- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
-- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
+
+## Знімки екрана
-## Знімки
+![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
-![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
+![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
-![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
+![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
-![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
+![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
-![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
diff --git a/docs/README-ZH.md b/docs/README-ZH.md
index 54b9c29a1417..4920ade6d960 100644
--- a/docs/README-ZH.md
+++ b/docs/README-ZH.md
@@ -8,7 +8,7 @@
[English ] | [Українська ] | [česky ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Ελληνικά ]
-Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
-RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
+RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
## 依赖
-桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
+桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。
+
+请自行下载Sciter动态库。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -133,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器
```
在Dockerfile的RUN apt update之前插入两行:
- RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list
- RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list
+ RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \
+ sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list
```
2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码:
@@ -207,12 +209,13 @@ target/release/rustdesk
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
-- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
+- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
-- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图
diff --git a/docs/SECURITY-NO.md b/docs/SECURITY-NO.md
new file mode 100644
index 000000000000..1f8dcb411bdc
--- /dev/null
+++ b/docs/SECURITY-NO.md
@@ -0,0 +1,9 @@
+# Sikkerhets Rettningslinjer
+
+## Reportering av en Sårbarhet
+
+Vi verdsetter pris på sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til å rapportere sårbarheter de oppdager til oss.
+Om du finner en sikkerhets sårbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved å sende oss en email til info@rustdesk.com.
+
+På dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver å løse ett stort problem. Vi trenger att du raporterer alle sårbarhetene
+annsvarfult så vi kan fortsettte å bygge ett en sikker applikasjon for hele felleskapet.
diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml
new file mode 100644
index 000000000000..0d3b33bb8c3d
--- /dev/null
+++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml
@@ -0,0 +1,59 @@
+
+
+ com.rustdesk.RustDesk
+
+ RustDesk
+
+ com.rustdesk.RustDesk.desktop
+ CC0-1.0
+ AGPL-3.0-only
+ RustDesk
+ Secure remote desktop access
+
+
+ RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration.
+
+
+ Works on Windows, macOS, Linux, iOS, Android, Web.
+ Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
+ Own your data, easily set up self-hosting solution on your infrastructure.
+ P2P connection with end-to-end encryption based on NaCl.
+ No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
+ We like to keep things simple and will strive to make simpler where possible.
+
+
+ For self-hosting setup instructions please go to our home page.
+
+
+
+ Utility
+
+
+
+ Remote desktop session
+ https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png
+
+
+
+ #d9eaf8
+ #0160ee
+
+ https://rustdesk.com
+ https://github.com/rustdesk/rustdesk/issues
+ https://github.com/rustdesk/rustdesk/wiki/FAQ
+ https://rustdesk.com/docs
+ https://ko-fi.com/rustdesk
+ https://github.com/rustdesk/rustdesk
+ https://github.com/rustdesk/rustdesk/tree/master/src/lang
+ https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md
+ https://rustdesk.com/docs/en/technical-support
+
+ 600
+ always
+
+
+ keyboard
+ pointing
+
+
+
\ No newline at end of file
diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json
index 6d7acb5b89ca..af1bc5fe74a1 100644
--- a/flatpak/rustdesk.json
+++ b/flatpak/rustdesk.json
@@ -1,19 +1,30 @@
{
"id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform",
- "runtime-version": "23.08",
+ "runtime-version": "24.08",
"sdk": "org.freedesktop.Sdk",
"command": "rustdesk",
- "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
+ "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
+ "rename-desktop-file": "rustdesk.desktop",
+ "rename-icon": "rustdesk",
"modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
- "xdotool.json",
{
- "name": "pam",
- "buildsystem": "simple",
- "build-commands": [
- "./configure --disable-selinux --prefix=/app && make -j4 install"
- ],
+ "name": "xdotool",
+ "no-autogen": true,
+ "make-install-args": ["PREFIX=${FLATPAK_DEST}"],
+ "sources": [
+ {
+ "type": "archive",
+ "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
+ "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
+ }
+ ]
+ },
+ {
+ "name": "pam",
+ "buildsystem": "autotools",
+ "config-opts": ["--disable-selinux"],
"sources": [
{
"type": "archive",
@@ -26,32 +37,24 @@
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
- "bsdtar -zxvf rustdesk.deb",
- "tar -xvf ./data.tar.xz",
- "cp -r ./usr/* /app/",
- "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
- "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
- "mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
- "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
- "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
- "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
+ "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -",
+ "cp -r usr/* /app/",
+ "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk"
],
- "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [
{
"type": "file",
- "path": "./rustdesk.deb"
+ "path": "rustdesk.deb"
},
{
"type": "file",
- "path": "../res/scalable.svg"
+ "path": "com.rustdesk.RustDesk.metainfo.xml"
}
]
}
],
"finish-args": [
"--share=ipc",
- "--socket=x11",
"--socket=fallback-x11",
"--socket=wayland",
"--share=network",
@@ -60,4 +63,4 @@
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Flatpak"
]
-}
+}
\ No newline at end of file
diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json
deleted file mode 100644
index d7f41bf0ec09..000000000000
--- a/flatpak/xdotool.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "xdotool",
- "buildsystem": "simple",
- "build-commands": [
- "make -j4 && PREFIX=./build make install",
- "cp -r ./build/* /app/"
- ],
- "sources": [
- {
- "type": "archive",
- "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
- "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
- }
- ]
-}
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
index 320eb3347c0e..c554251656a8 100644
--- a/flutter/android/app/build.gradle
+++ b/flutter/android/app/build.gradle
@@ -1,6 +1,9 @@
import com.google.protobuf.gradle.*
plugins {
id "com.google.protobuf" version "0.9.4"
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
}
def keystoreProperties = new Properties()
@@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
}
@@ -57,7 +51,7 @@ protobuf {
}
android {
- compileSdkVersion 33
+ compileSdkVersion 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -105,7 +99,6 @@ flutter {
dependencies {
implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5'
- implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
+ implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
implementation 'com.caverock:androidsvg-aar:1.4'
}
-
diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index 51015f74a831..47533612b3d5 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,13 @@
+
+
+
+
+
+
+
when (menuItem.itemId) {
@@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
openMainActivity()
true
}
+ idSyncClipboard -> {
+ syncClipboard()
+ true
+ }
idStopService -> {
stopMainService()
true
@@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
}
}
+ private fun syncClipboard() {
+ MainActivity.rdClipboardManager?.syncClipboard(false)
+ }
+
private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
index 3fcc72df346d..f53f95d6754e 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
@@ -280,20 +280,20 @@ class InputService : AccessibilityService() {
var textToCommit: String? = null
- if (keyboardMode == KeyboardMode.Legacy) {
- if (keyEvent.hasChr() && keyEvent.getDown()) {
+ // [down] indicates the key's state(down or up).
+ // [press] indicates a click event(down and up).
+ // https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688
+ if (keyEvent.hasSeq()) {
+ textToCommit = keyEvent.getSeq()
+ } else if (keyboardMode == KeyboardMode.Legacy) {
+ if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) {
val chr = keyEvent.getChr()
if (chr != null) {
textToCommit = String(Character.toChars(chr))
}
}
} else if (keyboardMode == KeyboardMode.Translate) {
- if (keyEvent.hasSeq() && keyEvent.getDown()) {
- val seq = keyEvent.getSeq()
- if (seq != null) {
- textToCommit = seq
- }
- }
+ } else {
}
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
@@ -320,6 +320,10 @@ class InputService : AccessibilityService() {
} else {
ke?.let { event ->
inputConnection.sendKeyEvent(event)
+ if (keyEvent.getPress()) {
+ val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
+ inputConnection.sendKeyEvent(actionUpEvent)
+ }
}
}
}
@@ -333,6 +337,10 @@ class InputService : AccessibilityService() {
for (item in possibleNodes) {
val success = trySendKeyEvent(event, item, textToCommit)
if (success) {
+ if (keyEvent.getPress()) {
+ val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
+ trySendKeyEvent(actionUpEvent, item, textToCommit)
+ }
break
}
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt
index 1e63df4054f5..ccb33195e9fb 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt
@@ -31,14 +31,12 @@ object KeyEventConverter {
}
var action = 0
- if (keyEventProto.getDown()) {
+ if (keyEventProto.getDown() || keyEventProto.getPress()) {
action = KeyEvent.ACTION_DOWN
} else {
action = KeyEvent.ACTION_UP
}
- // FIXME: The last parameter is the repeat count, not modifiers ?
- // https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
index 10c3d7c2d78f..5c54c18fb821 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
@@ -13,6 +13,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
+import android.content.ClipboardManager
+import android.os.Bundle
import android.os.Build
import android.os.IBinder
import android.util.Log
@@ -36,6 +38,9 @@ import kotlin.concurrent.thread
class MainActivity : FlutterActivity() {
companion object {
var flutterMethodChannel: MethodChannel? = null
+ private var _rdClipboardManager: RdClipboardManager? = null
+ val rdClipboardManager: RdClipboardManager?
+ get() = _rdClipboardManager;
}
private val channelTag = "mChannel"
@@ -85,6 +90,14 @@ class MainActivity : FlutterActivity() {
}
}
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (_rdClipboardManager == null) {
+ _rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
+ FFI.setClipboardManager(_rdClipboardManager!!)
+ }
+ }
+
override fun onDestroy() {
Log.e(logTag, "onDestroy")
mainService?.let {
@@ -207,6 +220,10 @@ class MainActivity : FlutterActivity() {
result.success(true)
}
+ "try_sync_clipboard" -> {
+ rdClipboardManager?.syncClipboard(true)
+ result.success(true)
+ }
GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
index 1a709747e25c..e9ec0975d1be 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -302,6 +302,8 @@ class MainService : Service() {
stopCapture()
FFI.refreshScreen()
startCapture()
+ } else {
+ FFI.refreshScreen()
}
}
@@ -431,6 +433,7 @@ class MainService : Service() {
checkMediaPermission()
_isStart = true
FFI.setFrameRawEnable("video",true)
+ MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
return true
}
@@ -439,6 +442,7 @@ class MainService : Service() {
Log.d(logTag, "Stop Capture")
FFI.setFrameRawEnable("video",false)
_isStart = false
+ MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
// release video
if (reuseVirtualDisplay) {
// The virtual display video projection can be paused by calling `setSurface(null)`.
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt
new file mode 100644
index 000000000000..8c9d85028402
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt
@@ -0,0 +1,197 @@
+package com.carriez.flutter_hbb
+
+import java.nio.ByteBuffer
+import java.util.Timer
+import java.util.TimerTask
+
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ClipboardManager
+import android.util.Log
+import androidx.annotation.Keep
+
+import hbb.MessageOuterClass.ClipboardFormat
+import hbb.MessageOuterClass.Clipboard
+import hbb.MessageOuterClass.MultiClipboards
+
+import ffi.FFI
+
+class RdClipboardManager(private val clipboardManager: ClipboardManager) {
+ private val logTag = "RdClipboardManager"
+ private val supportedMimeTypes = arrayOf(
+ ClipDescription.MIMETYPE_TEXT_PLAIN,
+ ClipDescription.MIMETYPE_TEXT_HTML
+ )
+
+ // 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`.
+ // 2. Avoid sending the clipboard data before enabling client clipboard.
+ // 1) Disable clipboard
+ // 2) Copy text "a"
+ // 3) Enable clipboard
+ // 4) Switch to another app
+ // 5) Switch back to the app
+ // 6) "a" should not be sent to the client, because it's copied before enabling clipboard
+ //
+ // It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`,
+ // though the `lastUpdatedClipData` will be set to null once.
+ private var lastUpdatedClipData: ClipData? = null
+ private var isClientEnabled = true;
+ private var _isCaptureStarted = false;
+
+ val isCaptureStarted: Boolean
+ get() = _isCaptureStarted
+
+ fun checkPrimaryClip(isClient: Boolean) {
+ val clipData = clipboardManager.primaryClip
+ if (clipData != null && clipData.itemCount > 0) {
+ // Only handle the first item in the clipboard for now.
+ val clip = clipData.getItemAt(0)
+ // Ignore the `isClipboardDataEqual()` check if it's a host operation.
+ // Because it's an action manually triggered by the user.
+ if (isClient) {
+ if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
+ Log.d(logTag, "Clipboard data is the same as last update, ignore")
+ return
+ }
+ }
+ val mimeTypeCount = clipData.description.getMimeTypeCount()
+ val mimeTypes = mutableListOf()
+ for (i in 0 until mimeTypeCount) {
+ mimeTypes.add(clipData.description.getMimeType(i))
+ }
+ var text: CharSequence? = null;
+ var html: String? = null;
+ if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
+ text = clip?.text
+ }
+ if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
+ text = clip?.text
+ html = clip?.htmlText
+ }
+ var count = 0
+ val clips = MultiClipboards.newBuilder()
+ if (text != null) {
+ val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString())
+ clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build())
+ count++
+ }
+ if (html != null) {
+ val content = com.google.protobuf.ByteString.copyFromUtf8(html)
+ clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build())
+ count++
+ }
+ if (count > 0) {
+ val clipsBytes = clips.build().toByteArray()
+ val isClientFlag = if (isClient) 1 else 0
+ val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply {
+ put(isClientFlag.toByte())
+ put(clipsBytes)
+ }
+ clipsBuf.flip()
+ lastUpdatedClipData = clipData
+ Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote")
+ FFI.onClipboardUpdate(clipsBuf)
+ }
+ }
+ }
+
+ private fun isSupportedMimeType(mimeType: String): Boolean {
+ return supportedMimeTypes.contains(mimeType)
+ }
+
+ private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean {
+ if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) {
+ return false
+ }
+ val mimeTypeCount = left.description.getMimeTypeCount()
+ for (i in 0 until mimeTypeCount) {
+ if (left.description.getMimeType(i) != right.description.getMimeType(i)) {
+ return false
+ }
+ }
+
+ if (left.itemCount != right.itemCount) {
+ return false
+ }
+ for (i in 0 until left.itemCount) {
+ val mimeType = left.description.getMimeType(i)
+ if (!isSupportedMimeType(mimeType)) {
+ continue
+ }
+ val leftItem = left.getItemAt(i)
+ val rightItem = right.getItemAt(i)
+ if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) {
+ if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+
+ fun setCaptureStarted(started: Boolean) {
+ _isCaptureStarted = started
+ }
+
+ @Keep
+ fun rustEnableClientClipboard(enable: Boolean) {
+ Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
+ isClientEnabled = enable
+ lastUpdatedClipData = null
+ }
+
+ fun syncClipboard(isClient: Boolean) {
+ Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled")
+ if (isClient && !isClientEnabled) {
+ return
+ }
+ checkPrimaryClip(isClient)
+ }
+
+ @Keep
+ fun rustUpdateClipboard(clips: ByteArray) {
+ val clips = MultiClipboards.parseFrom(clips)
+ var mimeTypes = mutableListOf()
+ var text: String? = null
+ var html: String? = null
+ for (clip in clips.getClipboardsList()) {
+ when (clip.format) {
+ ClipboardFormat.Text -> {
+ mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
+ text = String(clip.content.toByteArray(), Charsets.UTF_8)
+ }
+ ClipboardFormat.Html -> {
+ mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML)
+ html = String(clip.content.toByteArray(), Charsets.UTF_8)
+ }
+ ClipboardFormat.ImageRgba -> {
+ }
+ ClipboardFormat.ImagePng -> {
+ }
+ else -> {
+ Log.e(logTag, "Unsupported clipboard format: ${clip.format}")
+ }
+ }
+ }
+
+ val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray())
+ var item: ClipData.Item? = null
+ if (text == null) {
+ Log.e(logTag, "No text content in clipboard")
+ return
+ } else {
+ if (html == null) {
+ item = ClipData.Item(text)
+ } else {
+ item = ClipData.Item(text, html)
+ }
+ }
+ if (item == null) {
+ Log.e(logTag, "No item in clipboard")
+ return
+ }
+ val clipData = ClipData(clipDescription, item)
+ lastUpdatedClipData = clipData
+ clipboardManager.setPrimaryClip(clipData)
+ }
+}
diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt
index a7573bbf9eed..9f0f0216b727 100644
--- a/flutter/android/app/src/main/kotlin/ffi.kt
+++ b/flutter/android/app/src/main/kotlin/ffi.kt
@@ -5,12 +5,15 @@ package ffi
import android.content.Context
import java.nio.ByteBuffer
+import com.carriez.flutter_hbb.RdClipboardManager
+
object FFI {
init {
System.loadLibrary("rustdesk")
}
external fun init(ctx: Context)
+ external fun setClipboardManager(clipboardManager: RdClipboardManager)
external fun startServer(app_dir: String, custom_client_config: String)
external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer)
@@ -20,4 +23,6 @@ object FFI {
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
-}
\ No newline at end of file
+ external fun onClipboardUpdate(clips: ByteBuffer)
+ external fun isServiceClipboardEnabled(): Boolean
+}
diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle
index c6a77f36b171..401bea0096e2 100644
--- a/flutter/android/build.gradle
+++ b/flutter/android/build.gradle
@@ -1,18 +1,3 @@
-buildscript {
- ext.kotlin_version = '1.9.10'
- repositories {
- google()
- jcenter()
- maven { url 'https://jitpack.io' }
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:7.0.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.google.gms:google-services:4.3.14'
- }
-}
-
allprojects {
repositories {
google()
@@ -24,6 +9,8 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
project.evaluationDependsOn(':app')
}
diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties
index cc5527d781a7..cb576305fbe9 100644
--- a/flutter/android/gradle/wrapper/gradle-wrapper.properties
+++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle
index 44e62bcf06ae..c5fb685a1614 100644
--- a/flutter/android/settings.gradle
+++ b/flutter/android/settings.gradle
@@ -1,11 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
-def properties = new Properties()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-assert localPropertiesFile.exists()
-localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
-def flutterSdkPath = properties.getProperty("flutter.sdk")
-assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
-apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "7.3.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.9.10" apply false
+}
+
+include ":app"
diff --git a/flutter/assets/message_24dp_5F6368.svg b/flutter/assets/message_24dp_5F6368.svg
new file mode 100644
index 000000000000..5347a3d2d652
--- /dev/null
+++ b/flutter/assets/message_24dp_5F6368.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh
index e4210477e360..64fb9dad2a90 100755
--- a/flutter/build_android_deps.sh
+++ b/flutter/build_android_deps.sh
@@ -68,6 +68,7 @@ function build {
pushd "$SCRIPTDIR/.."
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
popd
+ head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh
index 2e0a20b6db60..ecfb444efea5 100755
--- a/flutter/build_fdroid.sh
+++ b/flutter/build_fdroid.sh
@@ -1,7 +1,5 @@
#!/bin/bash
-set -x
-
#
# Script to build F-Droid release of RustDesk
#
@@ -23,6 +21,43 @@ set -x
# + build: perform actual build of APK file
#
+# Start of functions
+
+# Install Flutter of version `VERSION` from Github repository
+# into directory `FLUTTER_DIR` and apply patches if needed
+
+prepare_flutter() {
+ VERSION="${1}"
+ FLUTTER_DIR="${2}"
+
+ if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then
+ git clone https://github.com/flutter/flutter "${FLUTTER_DIR}"
+ fi
+
+ pushd "${FLUTTER_DIR}"
+
+ git restore .
+ git checkout "${VERSION}"
+
+ # Patch flutter
+
+ if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then
+ git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
+ fi
+
+ flutter config --no-analytics
+
+ popd # ${FLUTTER_DIR}
+}
+
+# Start of script
+
+set -x
+
+# Note current working directory as root dir for patches
+
+ROOTDIR="${PWD}"
+
# Parse command-line arguments
VERNAME="${1}"
@@ -101,18 +136,31 @@ prebuild)
.env.CARGO_NDK_VERSION \
.github/workflows/flutter-build.yml)"
+ # Flutter used to compile main Rustdesk library
+
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
+
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
+ # Flutter used to compile Flutter<->Rust bridge files
+
+ CARGO_EXPAND_VERSION="$(yq -r \
+ .env.CARGO_EXPAND_VERSION \
+ .github/workflows/bridge.yml)"
+
+ FLUTTER_BRIDGE_VERSION="$(yq -r \
+ .env.FLUTTER_VERSION \
+ .github/workflows/bridge.yml)"
+
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \
- .github/workflows/flutter-build.yml)"
+ .github/workflows/bridge.yml)"
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
@@ -127,6 +175,7 @@ prebuild)
.github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
+ [ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then
@@ -163,24 +212,6 @@ prebuild)
sdkmanager --install "ndk;${NDK_VERSION}"
fi
- # Install Flutter
-
- if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
- pushd "${HOME}"
-
- git clone https://github.com/flutter/flutter
-
- pushd flutter
-
- git reset --hard "${FLUTTER_VERSION}"
-
- flutter config --no-analytics
-
- popd # flutter
-
- popd # ${HOME}
- fi
-
# Install Rust
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
@@ -205,14 +236,19 @@ prebuild)
cargo install \
cargo-ndk \
- --version "${CARGO_NDK_VERSION}"
+ --version "${CARGO_NDK_VERSION}" \
+ --locked
# Install rust bridge generator
- cargo install cargo-expand
+ cargo install \
+ cargo-expand \
+ --version "${CARGO_EXPAND_VERSION}" \
+ --locked
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
- --features "uuid"
+ --features "uuid" \
+ --locked
# Populate native vcpkg dependencies
@@ -275,12 +311,66 @@ prebuild)
git apply res/fdroid/patches/*.patch
+ # If Flutter version used to generate bridge files differs from Flutter
+ # version used to compile Rustdesk library, generate bridge using the
+ # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
+
+ if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
+ # Install Flutter bridge version
+
+ prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
+
+ # Save changes
+
+ git add .
+
+ # Edit pubspec to make flutter bridge version work
+
+ sed \
+ -i \
+ -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
+ flutter/pubspec.yaml
+
+ # Download Flutter dependencies
+
+ pushd flutter
+
+ flutter clean
+ flutter packages pub get
+
+ popd # flutter
+
+ # Generate FFI bindings
+
+ flutter_rust_bridge_codegen \
+ --rust-input ./src/flutter_ffi.rs \
+ --dart-output ./flutter/lib/generated_bridge.dart
+
+ # Add bridge files to save-list
+
+ git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.*
+
+ # Restore everything
+
+ git checkout '*'
+ git clean -dffx
+ git reset
+ fi
+
+ # Install Flutter version for RustDesk library build
+
+ prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
+
+ # gms is not in thoes files now, but we still keep the following line for future reference(maybe).
+
sed \
-i \
-e '/gms/d' \
flutter/android/build.gradle \
flutter/android/app/build.gradle
+ # `firebase_analytics` is not in these files now, but we still keep the following lines.
+
sed \
-i \
-e '/firebase_analytics/d' \
@@ -296,33 +386,6 @@ prebuild)
-e '/firebase/Id' \
flutter/lib/main.dart
- if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
- # Fix for android 3.13.9
- # https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
-
- sed \
- -i \
- -e 's/uni_links_desktop/#uni_links_desktop/g' \
- flutter/pubspec.yaml
-
- set --
-
- while read -r _1; do
- set -- "$@" "${_1}"
- done 0<<.a
-$(find flutter/lib/ -type f -name "*dart*")
-.a
-
- sed \
- -i \
- -e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
- "$@"
-
- set --
- fi
-
- sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
-
;;
build)
# build: perform actual build of APK file
@@ -334,9 +397,12 @@ build)
# '.github/workflows/flutter-build.yml'
#
+ # Flutter used to compile main Rustdesk library
+
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
+
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
@@ -372,16 +438,11 @@ build)
pushd flutter
+ flutter clean
flutter packages pub get
popd # flutter
- # Generate FFI bindings
-
- flutter_rust_bridge_codegen \
- --rust-input ./src/flutter_ffi.rs \
- --dart-output ./flutter/lib/generated_bridge.dart
-
# Build host android deps
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh
index a6468a0a8fb7..50f2f0056b41 100755
--- a/flutter/build_ios.sh
+++ b/flutter/build_ios.sh
@@ -2,4 +2,6 @@
# https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors
+cd $(dirname $(dirname $(which flutter)))
+git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
flutter build ipa --release
diff --git a/flutter/deploy.sh b/flutter/deploy.sh
deleted file mode 100755
index f6826fd8720d..000000000000
--- a/flutter/deploy.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-cd build/web/
-python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
-mv jds/dist/index.js ./
-mv jds/dist/vendor.js ./
-/bin/rm -rf js
-python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
-python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)'
-python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)'
-tar czf x *
-scp x sg:/tmp/
-ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
-/bin/rm x
-cd -
diff --git a/flutter/ios/Flutter/AppFrameworkInfo.plist b/flutter/ios/Flutter/AppFrameworkInfo.plist
index 7c5696400627..1dc6cf7652ba 100644
--- a/flutter/ios/Flutter/AppFrameworkInfo.plist
+++ b/flutter/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 12.0
+ 13.0
diff --git a/flutter/ios/Podfile b/flutter/ios/Podfile
index 321d7132cfb3..b71c436f2291 100644
--- a/flutter/ios/Podfile
+++ b/flutter/ios/Podfile
@@ -1,10 +1,7 @@
-# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
-
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
-platform :ios, '12.0'
+platform :ios, '13.0'
project 'Runner', {
'Debug' => :debug,
diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock
index 6cb5c9cff4c5..c9e9f9a2ffdd 100644
--- a/flutter/ios/Podfile.lock
+++ b/flutter/ios/Podfile.lock
@@ -133,10 +133,10 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
- url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
+ url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
-PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
+PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
COCOAPODS: 1.15.2
diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj
index acc2a09e7475..36dc89ea8a96 100644
--- a/flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -347,7 +347,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -491,7 +491,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -541,7 +541,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift
index 89e443af6d8b..d9333b7067be 100644
--- a/flutter/ios/Runner/AppDelegate.swift
+++ b/flutter/ios/Runner/AppDelegate.swift
@@ -1,7 +1,7 @@
import UIKit
import Flutter
-@UIApplicationMain
+@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh
index 579baaa6ddae..2d8410c7a4ef 100755
--- a/flutter/ios_arm64.sh
+++ b/flutter/ios_arm64.sh
@@ -1,2 +1,4 @@
#!/usr/bin/env bash
+cd $(dirname $(dirname $(which flutter)))
+git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index eba626343d03..2fde813bcdba 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
+import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
@@ -50,6 +51,9 @@ final isLinux = isLinux_;
final isDesktop = isDesktop_;
final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_;
+final isWebOnWindows = isWebOnWindows_;
+final isWebOnLinux = isWebOnLinux_;
+final isWebOnMacOs = isWebOnMacOS_;
var isMobile = isAndroid || isIOS;
var version = '';
int androidVersion = 0;
@@ -60,6 +64,9 @@ int androidVersion = 0;
// So we need to use this flag to enable/disable resizable.
bool _linuxWindowResizable = true;
+// Only used on Windows(window manager).
+bool _ignoreDevicePixelRatio = true;
+
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -347,6 +354,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white,
+ appBarTheme: AppBarTheme(
+ shadowColor: Colors.transparent,
+ ),
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
@@ -442,6 +452,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E),
+ appBarTheme: AppBarTheme(
+ shadowColor: Colors.transparent,
+ ),
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
@@ -545,9 +558,9 @@ class MyTheme {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
}
- static void changeDarkMode(ThemeMode mode) async {
+ static Future changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode);
- if (desktopType == DesktopType.main || isAndroid || isIOS) {
+ if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
if (mode == ThemeMode.system) {
await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme);
@@ -555,7 +568,7 @@ class MyTheme {
await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
- await bind.mainChangeTheme(dark: mode.toShortString());
+ if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
// Synchronize the window theme of the system.
updateSystemWindowTheme();
}
@@ -629,10 +642,30 @@ List supportedLocales = const [
Locale('da'),
Locale('eo'),
Locale('tr'),
- Locale('vi'),
- Locale('pl'),
Locale('kz'),
Locale('es'),
+ Locale('nl'),
+ Locale('nb'),
+ Locale('et'),
+ Locale('eu'),
+ Locale('bg'),
+ Locale('be'),
+ Locale('vn'),
+ Locale('uk'),
+ Locale('fa'),
+ Locale('ca'),
+ Locale('el'),
+ Locale('sv'),
+ Locale('sq'),
+ Locale('sr'),
+ Locale('th'),
+ Locale('sl'),
+ Locale('ro'),
+ Locale('lt'),
+ Locale('lv'),
+ Locale('ar'),
+ Locale('he'),
+ Locale('hr'),
];
String formatDurationToTime(Duration duration) {
@@ -651,10 +684,12 @@ closeConnection({String? id}) {
overlays: SystemUiOverlay.values);
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
+ stateGlobal.isInMainPage = true;
}();
} else {
if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
+ stateGlobal.isInMainPage = true;
} else {
final controller = Get.find();
controller.closeBy(id);
@@ -1057,6 +1092,49 @@ class CustomAlertDialog extends StatelessWidget {
}
}
+Widget createDialogContent(String text) {
+ final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
+ final List spans = [];
+ int start = 0;
+ bool hasLink = false;
+
+ linkRegExp.allMatches(text).forEach((match) {
+ hasLink = true;
+ if (match.start > start) {
+ spans.add(TextSpan(text: text.substring(start, match.start)));
+ }
+ spans.add(TextSpan(
+ text: match.group(0) ?? '',
+ style: TextStyle(
+ color: Colors.blue,
+ decoration: TextDecoration.underline,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ String linkText = match.group(0) ?? '';
+ linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
+ launchUrl(Uri.parse(linkText));
+ },
+ ));
+ start = match.end;
+ });
+
+ if (start < text.length) {
+ spans.add(TextSpan(text: text.substring(start)));
+ }
+
+ if (!hasLink) {
+ return SelectableText(text, style: const TextStyle(fontSize: 15));
+ }
+
+ return SelectableText.rich(
+ TextSpan(
+ style: TextStyle(color: Colors.black, fontSize: 15),
+ children: spans,
+ ),
+ );
+}
+
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
@@ -1099,33 +1177,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll();
}));
}
- if (reconnect != null && title == "Connection Error") {
+ if (reconnect != null &&
+ title == "Connection Error" &&
+ reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
- final button = reconnectTimeout != null
- ? Obx(() => _ReconnectCountDownButton(
- second: reconnectTimeout,
- onPressed: enabled.isTrue
- ? () {
- // Disable the button
- enabled.value = false;
- reconnect(dialogManager, sessionId, false);
- }
- : null,
- ))
- : Obx(
- () => dialogButton(
- 'Reconnect',
- isOutline: true,
- onPressed: enabled.isTrue
- ? () {
- // Disable the button
- enabled.value = false;
- reconnect(dialogManager, sessionId, false);
- }
- : null,
- ),
- );
+ final button = Obx(() => _ReconnectCountDownButton(
+ second: reconnectTimeout,
+ onPressed: enabled.isTrue
+ ? () {
+ // Disable the button
+ enabled.value = false;
+ reconnect(dialogManager, sessionId, false);
+ }
+ : null,
+ ));
buttons.insert(0, button);
}
if (link.isNotEmpty) {
@@ -1214,7 +1280,7 @@ Widget msgboxContent(String type, String title, String text) {
translate(title),
style: TextStyle(fontSize: 21),
).marginOnly(bottom: 10),
- Text(translateText(text), style: const TextStyle(fontSize: 15)),
+ createDialogContent(translateText(text)),
],
),
),
@@ -1548,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) {
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
-class OffsetDevicePixelRatio {
- Offset offset;
- final double devicePixelRatio;
- OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
-}
-
class LastWindowPosition {
double? width;
double? height;
@@ -1636,8 +1696,10 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async {
if (isFullscreen || isMaximized) {
setPreFrame();
} else {
- position = await windowManager.getPosition();
- sz = await windowManager.getSize();
+ position = await windowManager.getPosition(
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
+ sz = await windowManager.getSize(
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
break;
default:
@@ -1755,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) {
}
/// return null means center
-Future _adjustRestoreMainWindowOffset(
+Future _adjustRestoreMainWindowOffset(
double? left,
double? top,
double? width,
@@ -1769,13 +1831,9 @@ Future _adjustRestoreMainWindowOffset(
double? frameTop;
double? frameRight;
double? frameBottom;
- double devicePixelRatio = 1.0;
if (isDesktop || isWebDesktop) {
for (final screen in await window_size.getScreenList()) {
- if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
- devicePixelRatio = screen.scaleFactor;
- }
frameLeft = frameLeft == null
? screen.visibleFrame.left
: min(screen.visibleFrame.left, frameLeft);
@@ -1809,7 +1867,7 @@ Future _adjustRestoreMainWindowOffset(
top < frameTop!) {
return null;
} else {
- return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
+ return Offset(left, top);
}
}
@@ -1869,47 +1927,23 @@ Future restoreWindowPosition(WindowType type,
}
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
- final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
+ final offsetLeftTop = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth,
lpos.offsetHeight,
size.width,
size.height,
);
debugPrint(
- "restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
+ "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
switch (type) {
case WindowType.Main:
- // https://github.com/rustdesk/rustdesk/issues/8038
- // `setBounds()` in `window_manager` will use the current devicePixelRatio.
- // So we need to adjust the offset by the scale factor.
- // https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
- if (isWindows) {
- double? curDevicePixelRatio;
- Offset curPos = await windowManager.getPosition();
- for (final screen in await window_size.getScreenList()) {
- if (isPointInRect(curPos, screen.visibleFrame)) {
- curDevicePixelRatio = screen.scaleFactor;
- }
- }
- if (curDevicePixelRatio != null &&
- curDevicePixelRatio != 0 &&
- offsetDevicePixelRatio != null) {
- if (offsetDevicePixelRatio.devicePixelRatio != 0) {
- final scale =
- offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
- offsetDevicePixelRatio.offset =
- offsetDevicePixelRatio.offset.scale(scale, scale);
- debugPrint(
- "restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
- }
- }
- }
restorePos() async {
- if (offsetDevicePixelRatio == null) {
+ if (offsetLeftTop == null) {
await windowManager.center();
} else {
- await windowManager.setPosition(offsetDevicePixelRatio.offset);
+ await windowManager.setPosition(offsetLeftTop,
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
}
if (lpos.isMaximized == true) {
@@ -1918,20 +1952,39 @@ Future restoreWindowPosition(WindowType type,
await windowManager.maximize();
}
} else {
- if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
- await windowManager.setSize(size);
+ final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
+ if (isWindows) {
+ if (storeSize) {
+ // We need to set the window size first to avoid the incorrect size in some special cases.
+ // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
+ // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
+ // After restoring, the size will be incorrect.
+ // See known issue in https://github.com/rustdesk/rustdesk/pull/9840
+ await windowManager.setSize(size,
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
+ }
+ await restorePos();
+ if (storeSize) {
+ await windowManager.setSize(size,
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
+ }
+ } else {
+ if (storeSize) {
+ await windowManager.setSize(size,
+ ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
+ }
+ await restorePos();
}
- await restorePos();
}
return true;
default:
final wc = WindowController.fromWindowId(windowId!);
restoreFrame() async {
- if (offsetDevicePixelRatio == null) {
+ if (offsetLeftTop == null) {
await wc.center();
} else {
- final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
- offsetDevicePixelRatio.offset.dy, size.width, size.height);
+ final frame = Rect.fromLTWH(
+ offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
await wc.setFrame(frame);
}
}
@@ -1963,6 +2016,8 @@ Future restoreWindowPosition(WindowType type,
return false;
}
+var webInitialLink = "";
+
/// Initialize uni links for macos/windows
///
/// [Availability]
@@ -1979,7 +2034,12 @@ Future initUniLinks() async {
if (initialLink == null || initialLink.isEmpty) {
return false;
}
- return handleUriLink(uriString: initialLink);
+ if (isWeb) {
+ webInitialLink = initialLink;
+ return false;
+ } else {
+ return handleUriLink(uriString: initialLink);
+ }
} catch (err) {
debugPrintStack(label: "$err");
return false;
@@ -1992,7 +2052,7 @@ Future initUniLinks() async {
///
/// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
- if (isLinux) {
+ if (isLinux || isWeb) {
return null;
}
@@ -2185,7 +2245,10 @@ List? urlLinkToCmdArgs(Uri uri) {
}
}
- var key = uri.queryParameters["key"];
+ var queryParameters =
+ uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
+
+ var key = queryParameters["key"];
if (id != null) {
if (key != null) {
id = "$id?key=$key";
@@ -2194,7 +2257,7 @@ List? urlLinkToCmdArgs(Uri uri) {
if (isMobile) {
if (id != null) {
- final forceRelay = uri.queryParameters["relay"] != null;
+ final forceRelay = queryParameters["relay"] != null;
connect(Get.context!, id, forceRelay: forceRelay);
return null;
}
@@ -2204,7 +2267,7 @@ List? urlLinkToCmdArgs(Uri uri) {
if (command != null && id != null) {
args.add(command);
args.add(id);
- var param = uri.queryParameters;
+ var param = queryParameters;
String? password = param["password"];
if (password != null) args.addAll(['--password', password]);
String? switch_uuid = param["switch_uuid"];
@@ -2222,16 +2285,19 @@ connectMainDesktop(String id,
required bool isRDP,
bool? forceRelay,
String? password,
+ String? connToken,
bool? isSharedPassword}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id,
password: password,
isSharedPassword: isSharedPassword,
+ connToken: connToken,
forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP,
password: password,
isSharedPassword: isSharedPassword,
+ connToken: connToken,
forceRelay: forceRelay);
} else {
await rustDeskWinManager.newRemoteDesktop(id,
@@ -2251,6 +2317,7 @@ connect(BuildContext context, String id,
bool isRDP = false,
bool forceRelay = false,
String? password,
+ String? connToken,
bool? isSharedPassword}) async {
if (id == '') return;
if (!isDesktop || desktopType == DesktopType.main) {
@@ -2292,24 +2359,40 @@ connect(BuildContext context, String id,
'password': password,
'isSharedPassword': isSharedPassword,
'forceRelay': forceRelay,
+ 'connToken': connToken,
});
}
} else {
if (isFileTransfer) {
- if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
- if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
- return;
+ if (isAndroid) {
+ if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
+ if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
+ return;
+ }
}
}
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => FileManagerPage(
- id: id, password: password, isSharedPassword: isSharedPassword),
- ),
- );
+ if (isWeb) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) =>
+ desktop_file_manager.FileManagerPage(
+ id: id,
+ password: password,
+ isSharedPassword: isSharedPassword),
+ ),
+ );
+ } else {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => FileManagerPage(
+ id: id, password: password, isSharedPassword: isSharedPassword),
+ ),
+ );
+ }
} else {
- if (isWebDesktop) {
+ if (isWeb) {
Navigator.push(
context,
MaterialPageRoute(
@@ -2333,6 +2416,7 @@ connect(BuildContext context, String id,
);
}
}
+ stateGlobal.isInMainPage = false;
}
FocusScopeNode currentFocus = FocusScope.of(context);
@@ -2417,9 +2501,20 @@ Future onActiveWindowChanged() async {
// But the app will not close.
//
// No idea why we need to delay here, `terminate()` itself is also an async function.
- Future.delayed(Duration.zero, () {
- RdPlatformChannel.instance.terminate();
- });
+ //
+ // A quick workaround, use `Timer.periodic` to avoid the app not closing.
+ // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
+ // may not work since `Flutter 3.24.4`, see the following logs.
+ // A delay will allow the app to close.
+ //
+ //```
+ // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
+ // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
+ // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
+ // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
+ // ```
+ periodic_immediate(
+ Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
}
}
}
@@ -2512,7 +2607,7 @@ class ServerConfig {
config['relay'] = relayServer.trim();
config['api'] = apiServer.trim();
config['key'] = key.trim();
- return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
+ return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
.split('')
.reversed
.join();
@@ -2635,30 +2730,6 @@ Future osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}
-class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
- /// Creates scroll physics that does not let the user scroll.
- const DraggableNeverScrollableScrollPhysics({super.parent});
-
- @override
- DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
- return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
- }
-
- @override
- bool shouldAcceptUserOffset(ScrollMetrics position) {
- // TODO: find a better solution to check if the offset change is caused by the scrollbar.
- // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
- if (position is ScrollPositionWithSingleContext) {
- // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
- return position.activity is IdleScrollActivity;
- }
- return false;
- }
-
- @override
- bool get allowImplicitScrolling => false;
-}
-
Widget futureBuilder(
{required Future? future, required Widget Function(dynamic data) hasData}) {
return FutureBuilder(
@@ -2738,7 +2809,7 @@ Widget buildRemoteBlock(
onExit: (event) => block.value = false,
child: Stack(children: [
// scope block tab
- FocusScope(child: child, canRequestFocus: !block.value),
+ preventMouseKeyBuilder(child: child, block: block.value),
// mask block click, cm not block click and still use check_click_time to avoid block local click
if (mask)
Offstage(
@@ -2750,6 +2821,11 @@ Widget buildRemoteBlock(
));
}
+Widget preventMouseKeyBuilder({required Widget child, required bool block}) {
+ return ExcludeFocus(
+ excluding: block, child: AbsorbPointer(child: child, absorbing: block));
+}
+
Widget unreadMessageCountBuilder(RxInt? count,
{double? size, double? fontSize}) {
return Obx(() => Offstage(
@@ -2813,7 +2889,7 @@ Widget buildErrorBanner(BuildContext context,
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
- child: Text(
+ child: SelectableText(
translate(err.value),
),
)).marginSymmetric(vertical: 2),
@@ -3082,9 +3158,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
importConfig(List? controllers, List? errMsgs,
String? text) {
+ text = text?.trim();
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
+ if (isWeb || isIOS) {
+ sc.relayServer = '';
+ }
if (sc.idServer.isNotEmpty) {
Future success = setServerConfig(controllers, errMsgs, sc);
success.then((value) {
@@ -3429,7 +3509,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
);
}
-get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
+get windowResizeEdgeSize =>
+ isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details.
@@ -3490,3 +3571,70 @@ disableWindowMovable(int? windowId) {
WindowController.fromWindowId(windowId).setMovable(false);
}
}
+
+Widget netWorkErrorWidget() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(translate("network_error_tip")),
+ ElevatedButton(
+ onPressed: gFFI.userModel.refreshCurrentUser,
+ child: Text(translate("Retry")))
+ .marginSymmetric(vertical: 16),
+ SelectableText(gFFI.userModel.networkError.value,
+ style: TextStyle(fontSize: 11, color: Colors.red)),
+ ],
+ ));
+}
+
+List? get windowManagerEnableResizeEdges => isWindows
+ ? [
+ ResizeEdge.topLeft,
+ ResizeEdge.top,
+ ResizeEdge.topRight,
+ ]
+ : null;
+
+List? get subWindowManagerEnableResizeEdges => isWindows
+ ? [
+ SubWindowResizeEdge.topLeft,
+ SubWindowResizeEdge.top,
+ SubWindowResizeEdge.topRight,
+ ]
+ : null;
+
+void earlyAssert() {
+ assert('\1' == '1');
+}
+
+void checkUpdate() {
+ if (!isWeb) {
+ if (!bind.isCustomClient()) {
+ platformFFI.registerEventHandler(
+ kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
+ (Map evt) async {
+ if (evt['url'] is String) {
+ stateGlobal.updateUrl.value = evt['url'];
+ }
+ });
+ Timer(const Duration(seconds: 1), () async {
+ bind.mainGetSoftwareUpdateUrl();
+ });
+ }
+ }
+}
+
+// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535
+// For TextField, TextFormField
+extension WorkaroundFreezeLinuxMint on Widget {
+ Widget workaroundFreezeLinuxMint() {
+ // No need to check if is Linux Mint, because this workaround is harmless on other platforms.
+ if (isLinux) {
+ return ExcludeSemantics(child: this);
+ } else {
+ return this;
+ }
+ }
+}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 52b2e3d6296a..deed97bb30bd 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -1,5 +1,6 @@
import 'dart:math';
+import 'package:bot_toast/bot_toast.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart';
@@ -11,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
@@ -41,6 +43,8 @@ class _AddressBookState extends State {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
+ } else if (gFFI.userModel.networkError.isNotEmpty) {
+ return netWorkErrorWidget();
} else {
return Column(
children: [
@@ -59,15 +63,16 @@ class _AddressBookState extends State {
retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded(
- child: (isDesktop || isWebDesktop)
- ? _buildAddressBookDesktop()
- : _buildAddressBookMobile())
+ child: Obx(() => stateGlobal.isPortrait.isTrue
+ ? _buildAddressBookPortrait()
+ : _buildAddressBookLandscape()),
+ ),
],
);
}
});
- Widget _buildAddressBookDesktop() {
+ Widget _buildAddressBookLandscape() {
return Row(
children: [
Offstage(
@@ -104,7 +109,7 @@ class _AddressBookState extends State {
);
}
- Widget _buildAddressBookMobile() {
+ Widget _buildAddressBookPortrait() {
const padding = 8.0;
return Column(
children: [
@@ -237,14 +242,15 @@ class _AddressBookState extends State {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
}
},
- customButton: Container(
- height: isDesktop ? 48 : 40,
- child: Row(children: [
- Expanded(
- child: buildItem(gFFI.abModel.currentName.value, button: true)),
- Icon(Icons.arrow_drop_down),
- ]),
- ),
+ customButton: Obx(() => Container(
+ height: stateGlobal.isPortrait.isFalse ? 48 : 40,
+ child: Row(children: [
+ Expanded(
+ child:
+ buildItem(gFFI.abModel.currentName.value, button: true)),
+ Icon(Icons.arrow_drop_down),
+ ]),
+ )),
underline: Container(
height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1),
@@ -280,7 +286,7 @@ class _AddressBookState extends State {
borderRadius: BorderRadius.circular(8),
),
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
searchMatchFn: (item, searchValue) {
return item.value
@@ -311,13 +317,14 @@ class _AddressBookState extends State {
Widget _buildTags() {
return Obx(() {
- final List tags;
+ List tags;
if (gFFI.abModel.sortTags.value) {
tags = gFFI.abModel.currentAbTags.toList();
tags.sort();
} else {
- tags = gFFI.abModel.currentAbTags;
+ tags = gFFI.abModel.currentAbTags.toList();
}
+ tags = [kUntagged, ...tags].toList();
final editPermission = gFFI.abModel.current.canWrite();
tagBuilder(String e) {
return AddressBookTag(
@@ -333,8 +340,8 @@ class _AddressBookState extends State {
showActionMenu: editPermission);
}
- final gridView = DynamicGridView.builder(
- shrinkWrap: isMobile,
+ gridView(bool isPortrait) => DynamicGridView.builder(
+ shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
@@ -342,9 +349,9 @@ class _AddressBookState extends State {
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
- return (isDesktop || isWebDesktop)
- ? gridView
- : LimitedBox(maxHeight: maxHeight, child: gridView);
+ return Obx(() => stateGlobal.isPortrait.isFalse
+ ? gridView(false)
+ : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
});
}
@@ -354,7 +361,6 @@ class _AddressBookState extends State {
alignment: Alignment.topLeft,
child: AddressBookPeersView(
menuPadding: widget.menuPadding,
- getInitPeers: () => gFFI.abModel.currentAbPeers,
)),
);
}
@@ -504,20 +510,21 @@ class _AddressBookState extends State {
double marginBottom = 4;
row({required Widget lable, required Widget input}) {
- return Row(
- children: [
- !isMobile
- ? ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: lable.marginOnly(right: 10))
- : SizedBox.shrink(),
- Expanded(
- child: ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 200),
- child: input),
- ),
- ],
- ).marginOnly(bottom: !isMobile ? 8 : 0);
+ makeChild(bool isPortrait) => Row(
+ children: [
+ !isPortrait
+ ? ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 100),
+ child: lable.marginOnly(right: 10))
+ : SizedBox.shrink(),
+ Expanded(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 200),
+ child: input),
+ ),
+ ],
+ ).marginOnly(bottom: !isPortrait ? 8 : 0);
+ return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
}
return CustomAlertDialog(
@@ -540,24 +547,29 @@ class _AddressBookState extends State {
),
],
),
- input: TextField(
- controller: idController,
- inputFormatters: [IDTextInputFormatter()],
- decoration: InputDecoration(
- labelText: !isMobile ? null : translate('ID'),
- errorText: errorMsg,
- errorMaxLines: 5),
- )),
+ input: Obx(() => TextField(
+ controller: idController,
+ inputFormatters: [IDTextInputFormatter()],
+ decoration: InputDecoration(
+ labelText: stateGlobal.isPortrait.isFalse
+ ? null
+ : translate('ID'),
+ errorText: errorMsg,
+ errorMaxLines: 5),
+ ).workaroundFreezeLinuxMint())),
row(
lable: Text(
translate('Alias'),
style: style,
),
- input: TextField(
- controller: aliasController,
- decoration: InputDecoration(
- labelText: !isMobile ? null : translate('Alias'),
- )),
+ input: Obx(() => TextField(
+ controller: aliasController,
+ decoration: InputDecoration(
+ labelText: stateGlobal.isPortrait.isFalse
+ ? null
+ : translate('Alias'),
+ ),
+ ).workaroundFreezeLinuxMint()),
),
if (isCurrentAbShared)
row(
@@ -565,24 +577,28 @@ class _AddressBookState extends State {
translate('Password'),
style: style,
),
- input: TextField(
- controller: passwordController,
- obscureText: !passwordVisible,
- decoration: InputDecoration(
- labelText: !isMobile ? null : translate('Password'),
- suffixIcon: IconButton(
- icon: Icon(
- passwordVisible
- ? Icons.visibility
- : Icons.visibility_off,
- color: MyTheme.lightTheme.primaryColor),
- onPressed: () {
- setState(() {
- passwordVisible = !passwordVisible;
- });
- },
+ input: Obx(
+ () => TextField(
+ controller: passwordController,
+ obscureText: !passwordVisible,
+ decoration: InputDecoration(
+ labelText: stateGlobal.isPortrait.isFalse
+ ? null
+ : translate('Password'),
+ suffixIcon: IconButton(
+ icon: Icon(
+ passwordVisible
+ ? Icons.visibility
+ : Icons.visibility_off,
+ color: MyTheme.lightTheme.primaryColor),
+ onPressed: () {
+ setState(() {
+ passwordVisible = !passwordVisible;
+ });
+ },
+ ),
),
- ),
+ ).workaroundFreezeLinuxMint(),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
@@ -655,6 +671,14 @@ class _AddressBookState extends State {
} else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(',');
+ for (var t in [kUntagged, translate(kUntagged)]) {
+ if (tags.contains(t)) {
+ BotToast.showText(
+ contentColor: Colors.red, text: 'Tag name cannot be "$t"');
+ isInProgress = false;
+ return;
+ }
+ }
gFFI.abModel.addTags(tags);
// final currentPeers
}
@@ -680,7 +704,7 @@ class _AddressBookState extends State {
),
controller: controller,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
),
@@ -727,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
}
const double radius = 8;
+ final isUnTagged = name == kUntagged;
+ final showAction = showActionMenu && !isUnTagged;
return GestureDetector(
onTap: onTap,
- onTapDown: showActionMenu ? setPosition : null,
- onSecondaryTapDown: showActionMenu ? setPosition : null,
- onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
- onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
+ onTapDown: showAction ? setPosition : null,
+ onSecondaryTapDown: showAction ? setPosition : null,
+ onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
+ onLongPress: showAction ? () => _showMenu(context, pos) : null,
child: Obx(() => Container(
decoration: BoxDecoration(
color: tags.contains(name)
@@ -744,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
child: IntrinsicWidth(
child: Row(
children: [
- Container(
- width: radius,
- height: radius,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: tags.contains(name)
- ? Colors.white
- : gFFI.abModel.getCurrentAbTagColor(name)),
- ).marginOnly(right: radius / 2),
+ if (!isUnTagged)
+ Container(
+ width: radius,
+ height: radius,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: tags.contains(name)
+ ? Colors.white
+ : gFFI.abModel.getCurrentAbTagColor(name)),
+ ).marginOnly(right: radius / 2),
Expanded(
- child: Text(name,
+ child: Text(isUnTagged ? translate(name) : name,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: tags.contains(name) ? Colors.white : null)),
diff --git a/flutter/lib/common/widgets/audio_input.dart b/flutter/lib/common/widgets/audio_input.dart
index 1db4391270cb..1f8f1a8b94e6 100644
--- a/flutter/lib/common/widgets/audio_input.dart
+++ b/flutter/lib/common/widgets/audio_input.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
-const _kWindowsSystemSound = 'System Sound';
+const _kSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
@@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget {
: super(key: key);
static String getDefault() {
- if (isWindows) return translate('System Sound');
+ if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
return '';
}
@@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget {
static Future> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List devices = (await bind.mainGetSoundInputs()).toList();
- if (isWindows) {
- devices.insert(0, translate(_kWindowsSystemSound));
+ if (bind.mainAudioSupportLoopback()) {
+ devices.insert(0, translate(_kSystemSound));
}
String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current};
diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart
index 07a11904da17..978d053df4be 100644
--- a/flutter/lib/common/widgets/autocomplete.dart
+++ b/flutter/lib/common/widgets/autocomplete.dart
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
- message: isMobile
+ message: !(isDesktop || isWebDesktop)
? ''
: widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index b6611d3ede39..4b0954d40b19 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape {
);
},
),
- );
+ ).workaroundFreezeLinuxMint();
return SelectionArea(child: chat);
}),
],
diff --git a/flutter/lib/common/widgets/connection_page_title.dart b/flutter/lib/common/widgets/connection_page_title.dart
new file mode 100644
index 000000000000..ba03c2656960
--- /dev/null
+++ b/flutter/lib/common/widgets/connection_page_title.dart
@@ -0,0 +1,38 @@
+import 'package:auto_size_text/auto_size_text.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import '../../common.dart';
+
+Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
+ return Row(
+ children: [
+ Expanded(
+ child: Row(
+ children: [
+ AutoSizeText(
+ translate('Control Remote Desktop'),
+ maxLines: 1,
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge
+ ?.merge(TextStyle(height: 1)),
+ ).marginOnly(right: 4),
+ Tooltip(
+ waitDuration: Duration(milliseconds: 300),
+ message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
+ child: Icon(
+ Icons.help_outline_outlined,
+ size: 16,
+ color: Theme.of(context)
+ .textTheme
+ .titleLarge
+ ?.color
+ ?.withOpacity(0.5),
+ ),
+ ),
+ ],
+ )),
+ ],
+ );
+}
diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart
index 99ece2434bf4..dafc23b448b9 100644
--- a/flutter/lib/common/widgets/custom_password.dart
+++ b/flutter/lib/common/widgets/custom_password.dart
@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase');
@override
bool validate(String value) {
- return value.contains(RegExp(r'[A-Z]'));
+ return value.runes.any((int rune) {
+ var character = String.fromCharCode(rune);
+ return character.toUpperCase() == character &&
+ character.toLowerCase() != character;
+ });
}
}
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
@override
bool validate(String value) {
- return value.contains(RegExp(r'[a-z]'));
+ return value.runes.any((int rune) {
+ var character = String.fromCharCode(rune);
+ return character.toLowerCase() == character &&
+ character.toUpperCase() != character;
+ });
}
}
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index f140a68b0779..0fb8d552d7ab 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
@@ -139,7 +140,7 @@ void changeIdDialog() {
msg = '';
});
},
- ),
+ ).workaroundFreezeLinuxMint(),
const SizedBox(
height: 8.0,
),
@@ -200,13 +201,14 @@ void changeWhiteList({Function()? callback}) async {
children: [
Expanded(
child: TextField(
- maxLines: null,
- decoration: InputDecoration(
- errorText: msg.isEmpty ? null : translate(msg),
- ),
- controller: controller,
- enabled: !isOptFixed,
- autofocus: true),
+ maxLines: null,
+ decoration: InputDecoration(
+ errorText: msg.isEmpty ? null : translate(msg),
+ ),
+ controller: controller,
+ enabled: !isOptFixed,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -286,22 +288,23 @@ Future changeDirectAccessPort(
children: [
Expanded(
child: TextField(
- maxLines: null,
- keyboardType: TextInputType.number,
- decoration: InputDecoration(
- hintText: '21118',
- isCollapsed: true,
- prefix: Text('$currentIP : '),
- suffix: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.clear, size: 16),
- onPressed: () => controller.clear())),
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(
- r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
- ],
- controller: controller,
- autofocus: true),
+ maxLines: null,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ hintText: '21118',
+ isCollapsed: true,
+ prefix: Text('$currentIP : '),
+ suffix: IconButton(
+ padding: EdgeInsets.zero,
+ icon: const Icon(Icons.clear, size: 16),
+ onPressed: () => controller.clear())),
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(
+ r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
+ ],
+ controller: controller,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -334,21 +337,22 @@ Future changeAutoDisconnectTimeout(String old) async {
children: [
Expanded(
child: TextField(
- maxLines: null,
- keyboardType: TextInputType.number,
- decoration: InputDecoration(
- hintText: '10',
- isCollapsed: true,
- suffix: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.clear, size: 16),
- onPressed: () => controller.clear())),
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(
- r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
- ],
- controller: controller,
- autofocus: true),
+ maxLines: null,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ hintText: '10',
+ isCollapsed: true,
+ suffix: IconButton(
+ padding: EdgeInsets.zero,
+ icon: const Icon(Icons.clear, size: 16),
+ onPressed: () => controller.clear())),
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(
+ r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
+ ],
+ controller: controller,
+ autofocus: true)
+ .workaroundFreezeLinuxMint(),
),
],
),
@@ -380,6 +384,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode;
final TextInputType? keyboardType;
final List? inputFormatters;
+ final int? maxLength;
static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -397,6 +402,7 @@ class DialogTextField extends StatelessWidget {
this.hintText,
this.keyboardType,
this.inputFormatters,
+ this.maxLength,
required this.title,
required this.controller})
: super(key: key);
@@ -423,7 +429,8 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
- ),
+ maxLength: maxLength,
+ ).workaroundFreezeLinuxMint(),
),
],
).paddingSymmetric(vertical: 4.0);
@@ -679,6 +686,8 @@ class PasswordWidget extends StatefulWidget {
this.reRequestFocus = false,
this.hintText,
this.errorText,
+ this.title,
+ this.maxLength,
}) : super(key: key);
final TextEditingController controller;
@@ -686,6 +695,8 @@ class PasswordWidget extends StatefulWidget {
final bool reRequestFocus;
final String? hintText;
final String? errorText;
+ final String? title;
+ final int? maxLength;
@override
State createState() => _PasswordWidgetState();
@@ -729,7 +740,7 @@ class _PasswordWidgetState extends State {
@override
Widget build(BuildContext context) {
return DialogTextField(
- title: translate(DialogTextField.kPasswordTitle),
+ title: translate(widget.title ?? DialogTextField.kPasswordTitle),
hintText: translate(widget.hintText ?? 'Enter your password'),
controller: widget.controller,
prefixIcon: DialogTextField.kPasswordIcon,
@@ -748,6 +759,7 @@ class _PasswordWidgetState extends State {
obscureText: !_passwordVisible,
errorText: widget.errorText,
focusNode: _focusNode,
+ maxLength: widget.maxLength,
);
}
}
@@ -1121,7 +1133,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value,
),
],
- ).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
+ ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10),
],
),
@@ -1492,7 +1504,7 @@ showAuditDialog(FFI ffi) async {
maxLength: 256,
controller: controller,
focusNode: focusNode,
- )),
+ ).workaroundFreezeLinuxMint()),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
@@ -1739,7 +1751,7 @@ void renameDialog(
autofocus: true,
decoration: InputDecoration(labelText: translate('Name')),
validator: validator,
- ),
+ ).workaroundFreezeLinuxMint(),
),
),
// NOT use Offstage to wrap LinearProgressIndicator
@@ -1799,7 +1811,7 @@ void changeBot({Function()? callback}) async {
decoration: InputDecoration(
hintText: translate('Token'),
),
- );
+ ).workaroundFreezeLinuxMint();
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
@@ -1829,6 +1841,7 @@ void changeBot({Function()? callback}) async {
void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: "");
+ await bind.mainClearTrustedDevices();
callback?.call();
return;
}
@@ -1896,6 +1909,7 @@ void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
final RxBool submitReady = false.obs;
+ final RxBool trustThisDevice = false.obs;
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
@@ -1905,7 +1919,7 @@ void enter2FaDialog(
}
submit() {
- gFFI.send2FA(sessionId, controller.text.trim());
+ gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
close();
dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection);
@@ -1919,9 +1933,27 @@ void enter2FaDialog(
onChanged: () => submitReady.value = codeField.isReady,
);
+ final trustField = Obx(() => CheckboxListTile(
+ contentPadding: const EdgeInsets.all(0),
+ dense: true,
+ controlAffinity: ListTileControlAffinity.leading,
+ title: Text(translate("Trust this device")),
+ value: trustThisDevice.value,
+ onChanged: (value) {
+ if (value == null) return;
+ trustThisDevice.value = value;
+ },
+ ));
+
return CustomAlertDialog(
title: Text(translate('enter-2fa-title')),
- content: codeField,
+ content: Column(
+ children: [
+ codeField,
+ if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
+ trustField,
+ ],
+ ),
actions: [
dialogButton('Cancel',
onPressed: cancel,
@@ -2149,7 +2181,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
},
),
),
- ),
+ ).workaroundFreezeLinuxMint(),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
@@ -2216,3 +2248,255 @@ void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
);
});
}
+
+void changeUnlockPinDialog(String oldPin, Function() callback) {
+ final pinController = TextEditingController(text: oldPin);
+ final confirmController = TextEditingController(text: oldPin);
+ String? pinErrorText;
+ String? confirmationErrorText;
+ final maxLength = bind.mainMaxEncryptLen();
+ gFFI.dialogManager.show((setState, close, context) {
+ submit() async {
+ pinErrorText = null;
+ confirmationErrorText = null;
+ final pin = pinController.text.trim();
+ final confirm = confirmController.text.trim();
+ if (pin != confirm) {
+ setState(() {
+ confirmationErrorText =
+ translate('The confirmation is not identical.');
+ });
+ return;
+ }
+ final errorMsg = bind.mainSetUnlockPin(pin: pin);
+ if (errorMsg != '') {
+ setState(() {
+ pinErrorText = translate(errorMsg);
+ });
+ return;
+ }
+ callback.call();
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Text(translate("Set PIN")),
+ content: Column(
+ children: [
+ DialogTextField(
+ title: 'PIN',
+ controller: pinController,
+ obscureText: true,
+ errorText: pinErrorText,
+ maxLength: maxLength,
+ ),
+ DialogTextField(
+ title: translate('Confirmation'),
+ controller: confirmController,
+ obscureText: true,
+ errorText: confirmationErrorText,
+ maxLength: maxLength,
+ )
+ ],
+ ).marginOnly(bottom: 12),
+ actions: [
+ dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
+ dialogButton(translate("OK"), onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+}
+
+void checkUnlockPinDialog(String correctPin, Function() passCallback) {
+ final controller = TextEditingController();
+ String? errorText;
+ gFFI.dialogManager.show((setState, close, context) {
+ submit() async {
+ final pin = controller.text.trim();
+ if (correctPin != pin) {
+ setState(() {
+ errorText = translate('Wrong PIN');
+ });
+ return;
+ }
+ passCallback.call();
+ close();
+ }
+
+ return CustomAlertDialog(
+ content: Row(
+ children: [
+ Expanded(
+ child: PasswordWidget(
+ title: 'PIN',
+ controller: controller,
+ errorText: errorText,
+ hintText: '',
+ ))
+ ],
+ ).marginOnly(bottom: 12),
+ actions: [
+ dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
+ dialogButton(translate("OK"), onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+}
+
+void confrimDeleteTrustedDevicesDialog(
+ RxList trustedDevices, RxList selectedDevices) {
+ CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
+ () async {
+ if (selectedDevices.isEmpty) return;
+ if (selectedDevices.length == trustedDevices.length) {
+ await bind.mainClearTrustedDevices();
+ trustedDevices.clear();
+ selectedDevices.clear();
+ } else {
+ final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
+ await bind.mainRemoveTrustedDevices(json: json);
+ trustedDevices.removeWhere((element) {
+ return selectedDevices.contains(element.hwid);
+ });
+ selectedDevices.clear();
+ }
+ });
+}
+
+void manageTrustedDeviceDialog() async {
+ RxList trustedDevices = (await TrustedDevice.get()).obs;
+ RxList selectedDevices = RxList.empty();
+ gFFI.dialogManager.show((setState, close, context) {
+ return CustomAlertDialog(
+ title: Text(translate("Manage trusted devices")),
+ content: trustedDevicesTable(trustedDevices, selectedDevices),
+ actions: [
+ Obx(() => dialogButton(translate("Delete"),
+ onPressed: selectedDevices.isEmpty
+ ? null
+ : () {
+ confrimDeleteTrustedDevicesDialog(
+ trustedDevices,
+ selectedDevices,
+ );
+ },
+ isOutline: false)
+ .marginOnly(top: 12)),
+ dialogButton(translate("Close"), onPressed: close, isOutline: true)
+ .marginOnly(top: 12),
+ ],
+ onCancel: close,
+ );
+ });
+}
+
+class TrustedDevice {
+ late final Uint8List hwid;
+ late final int time;
+ late final String id;
+ late final String name;
+ late final String platform;
+
+ TrustedDevice.fromJson(Map json) {
+ final hwidList = json['hwid'] as List;
+ hwid = Uint8List.fromList(hwidList.cast());
+ time = json['time'];
+ id = json['id'];
+ name = json['name'];
+ platform = json['platform'];
+ }
+
+ String daysRemaining() {
+ final expiry = time + 90 * 24 * 60 * 60 * 1000;
+ final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
+ if (remaining < 0) {
+ return '0';
+ }
+ return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
+ }
+
+ static Future> get() async {
+ final List devices = List.empty(growable: true);
+ try {
+ final devicesJson = await bind.mainGetTrustedDevices();
+ if (devicesJson.isNotEmpty) {
+ final devicesList = json.decode(devicesJson);
+ if (devicesList is List) {
+ for (var device in devicesList) {
+ devices.add(TrustedDevice.fromJson(device));
+ }
+ }
+ }
+ } catch (e) {
+ print(e.toString());
+ }
+ devices.sort((a, b) => b.time.compareTo(a.time));
+ return devices;
+ }
+}
+
+Widget trustedDevicesTable(
+ RxList devices, RxList selectedDevices) {
+ RxBool selectAll = false.obs;
+ setSelectAll() {
+ if (selectedDevices.isNotEmpty &&
+ selectedDevices.length == devices.length) {
+ selectAll.value = true;
+ } else {
+ selectAll.value = false;
+ }
+ }
+
+ devices.listen((_) {
+ setSelectAll();
+ });
+ selectedDevices.listen((_) {
+ setSelectAll();
+ });
+ return FittedBox(
+ child: Obx(() => DataTable(
+ columns: [
+ DataColumn(
+ label: Checkbox(
+ value: selectAll.value,
+ onChanged: (value) {
+ if (value == true) {
+ selectedDevices.clear();
+ selectedDevices.addAll(devices.map((e) => e.hwid));
+ } else {
+ selectedDevices.clear();
+ }
+ },
+ )),
+ DataColumn(label: Text(translate('Platform'))),
+ DataColumn(label: Text(translate('ID'))),
+ DataColumn(label: Text(translate('Username'))),
+ DataColumn(label: Text(translate('Days remaining'))),
+ ],
+ rows: devices.map((device) {
+ return DataRow(cells: [
+ DataCell(Checkbox(
+ value: selectedDevices.contains(device.hwid),
+ onChanged: (value) {
+ if (value == null) return;
+ if (value) {
+ selectedDevices.remove(device.hwid);
+ selectedDevices.add(device.hwid);
+ } else {
+ selectedDevices.remove(device.hwid);
+ }
+ },
+ )),
+ DataCell(Text(device.platform)),
+ DataCell(Text(device.id)),
+ DataCell(Text(device.name)),
+ DataCell(Text(device.daysRemaining())),
+ ]);
+ }).toList(),
+ )),
+ );
+}
diff --git a/flutter/lib/common/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart
index 6c269656722f..b3cfeae6e632 100644
--- a/flutter/lib/common/widgets/gestures.dart
+++ b/flutter/lib/common/widgets/gestures.dart
@@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
// end
switch (_currentState) {
case GestureState.oneFingerPan:
- debugPrint("TwoFingerState.pan onEnd");
+ debugPrint("OneFingerState.pan onEnd");
if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d));
}
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index 71f3dacc3b2c..50e05cde5881 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -678,7 +678,7 @@ Future verificationCodeDialog(
labelText: "Email", prefixIcon: Icon(Icons.email)),
readOnly: true,
controller: TextEditingController(text: user?.email),
- )),
+ ).workaroundFreezeLinuxMint()),
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
codeField,
/*
diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart
index e139ce700d22..359fbc7f7212 100644
--- a/flutter/lib/common/widgets/my_group.dart
+++ b/flutter/lib/common/widgets/my_group.dart
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -30,6 +31,8 @@ class _MyGroupState extends State {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
+ } else if (gFFI.userModel.networkError.isNotEmpty) {
+ return netWorkErrorWidget();
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),
@@ -43,15 +46,15 @@ class _MyGroupState extends State {
retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(
- child: (isDesktop || isWebDesktop)
- ? _buildDesktop()
- : _buildMobile())
+ child: Obx(() => stateGlobal.isPortrait.isTrue
+ ? _buildPortrait()
+ : _buildLandscape())),
],
);
});
}
- Widget _buildDesktop() {
+ Widget _buildLandscape() {
return Row(
children: [
Container(
@@ -80,14 +83,14 @@ class _MyGroupState extends State {
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
- menuPadding: widget.menuPadding,
- getInitPeers: () => gFFI.groupModel.peers)),
+ menuPadding: widget.menuPadding,
+ )),
)
],
);
}
- Widget _buildMobile() {
+ Widget _buildPortrait() {
return Column(
children: [
Container(
@@ -112,8 +115,8 @@ class _MyGroupState extends State {
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
- menuPadding: widget.menuPadding,
- getInitPeers: () => gFFI.groupModel.peers)),
+ menuPadding: widget.menuPadding,
+ )),
)
],
);
@@ -142,7 +145,7 @@ class _MyGroupState extends State {
border: InputBorder.none,
isDense: true,
),
- )),
+ ).workaroundFreezeLinuxMint()),
],
);
}
@@ -157,14 +160,14 @@ class _MyGroupState extends State {
}
return true;
}).toList();
- final listView = ListView.builder(
- shrinkWrap: isMobile,
+ listView(bool isPortrait) => ListView.builder(
+ shrinkWrap: isPortrait,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
- return (isDesktop || isWebDesktop)
- ? listView
- : LimitedBox(maxHeight: maxHeight, child: listView);
+ return Obx(() => stateGlobal.isPortrait.isFalse
+ ? listView(false)
+ : LimitedBox(maxHeight: maxHeight, child: listView(true)));
});
}
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index f9dd73bbf6b8..b4bca12a9e67 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
@override
Widget build(BuildContext context) {
super.build(context);
- if (isDesktop || isWebDesktop) {
- return _buildDesktop();
- } else {
- return _buildMobile();
- }
+ return Obx(() =>
+ stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
}
- Widget _buildMobile() {
- final peer = super.widget.peer;
+ Widget gestureDetector({required Widget child}) {
final PeerTabModel peerTabModel = Provider.of(context);
- return Card(
- margin: EdgeInsets.symmetric(horizontal: 2),
- child: GestureDetector(
- onTap: () {
- if (peerTabModel.multiSelectionMode) {
- peerTabModel.select(peer);
+ final peer = super.widget.peer;
+ return GestureDetector(
+ onDoubleTap: peerTabModel.multiSelectionMode
+ ? null
+ : () => widget.connect(context, peer.id),
+ onTap: () {
+ if (peerTabModel.multiSelectionMode) {
+ peerTabModel.select(peer);
+ } else {
+ if (isMobile) {
+ widget.connect(context, peer.id);
} else {
- if (!isWebDesktop) {
- connectInPeerTab(context, peer, widget.tab);
- }
+ peerTabModel.select(peer);
}
- },
- onDoubleTap: isWebDesktop
- ? () => connectInPeerTab(context, peer, widget.tab)
- : null,
- onLongPress: () {
- peerTabModel.select(peer);
- },
+ }
+ },
+ onLongPress: () => peerTabModel.select(peer),
+ child: child);
+ }
+
+ Widget _buildPortrait() {
+ final peer = super.widget.peer;
+ return Card(
+ margin: EdgeInsets.symmetric(horizontal: 2),
+ child: gestureDetector(
child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: _buildPeerTile(context, peer, null)),
));
}
- Widget _buildDesktop() {
- final PeerTabModel peerTabModel = Provider.of(context);
+ Widget _buildLandscape() {
final peer = super.widget.peer;
var deco = Rx(
BoxDecoration(
@@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard>
),
);
},
- child: GestureDetector(
- onDoubleTap:
- peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
- ? null
- : () => widget.connect(context, peer.id),
- onTap: () => peerTabModel.select(peer),
- onLongPress: () => peerTabModel.select(peer),
+ child: gestureDetector(
child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))),
);
}
- Widget _buildPeerTile(
- BuildContext context, Peer peer, Rx? deco) {
- hideUsernameOnCard ??=
- bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
+ makeChild(bool isPortrait, Peer peer) {
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
- final child = Row(
+ return Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
- borderRadius: isMobile
+ borderRadius: isPortrait
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
@@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
),
),
alignment: Alignment.center,
- width: isMobile ? 50 : 42,
- height: isMobile ? 50 : null,
+ width: isPortrait ? 50 : 42,
+ height: isPortrait ? 50 : null,
child: Stack(
children: [
- getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
+ getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6),
if (_shouldBuildPasswordIcon(peer))
Positioned(
@@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column(
children: [
Row(children: [
- getOnline(isMobile ? 4 : 8, peer.online),
+ getOnline(isPortrait ? 4 : 8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
- ]).marginOnly(top: isMobile ? 0 : 2),
+ ]).marginOnly(top: isPortrait ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
- style: isMobile ? null : greyStyle,
+ style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
@@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard>
],
).marginOnly(top: 2),
),
- isMobile
- ? checkBoxOrActionMoreMobile(peer)
- : checkBoxOrActionMoreDesktop(peer, isTile: true),
+ isPortrait
+ ? checkBoxOrActionMorePortrait(peer)
+ : checkBoxOrActionMoreLandscape(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
);
+ }
+
+ Widget _buildPeerTile(
+ BuildContext context, Peer peer, Rx? deco) {
+ hideUsernameOnCard ??=
+ bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
- message: isMobile
+ message: !(isDesktop || isWebDesktop)
? ''
: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
- deco == null
- ? child
- : Obx(
- () => Container(
+ Obx(
+ () => deco == null
+ ? makeChild(stateGlobal.isPortrait.isTrue, peer)
+ : Container(
foregroundDecoration: deco.value,
- child: child,
+ child: makeChild(stateGlobal.isPortrait.isTrue, peer),
),
- ),
+ ),
if (colors.isNotEmpty)
- Positioned(
- top: 2,
- right: isMobile ? 20 : 10,
- child: CustomPaint(
- painter: TagPainter(radius: 3, colors: colors),
- ),
- )
+ Obx(() => Positioned(
+ top: 2,
+ right: stateGlobal.isPortrait.isTrue ? 20 : 10,
+ child: CustomPaint(
+ painter: TagPainter(radius: 3, colors: colors),
+ ),
+ ))
]),
);
}
@@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard>
color: Colors.transparent,
elevation: 0,
margin: EdgeInsets.zero,
+ // to-do: memory leak here, more investigation needed.
+ // Continious rebuilds of `Obx()` will cause memory leak here.
+ // The simple demo does not have this issue.
child: Obx(
() => Container(
foregroundDecoration: deco.value,
@@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall,
)),
]).paddingSymmetric(vertical: 8)),
- checkBoxOrActionMoreDesktop(peer, isTile: false),
+ checkBoxOrActionMoreLandscape(peer, isTile: false),
],
).paddingSymmetric(horizontal: 12.0),
)
@@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
}
}
- Widget checkBoxOrActionMoreMobile(Peer peer) {
+ Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) {
@@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
}
}
- Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
+ Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) {
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
BuildContext context) async {
final List> menuItems = [
_connectAction(context),
- if (!isWeb) _transferFileAction(context),
+ _transferFileAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
BuildContext context) async {
final List> menuItems = [
_connectAction(context),
- if (!isWeb) _transferFileAction(context),
+ _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
BuildContext context) async {
final List> menuItems = [
_connectAction(context),
- if (!isWeb) _transferFileAction(context),
+ _transferFileAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
BuildContext context) async {
final List> menuItems = [
_connectAction(context),
- if (!isWeb) _transferFileAction(context),
+ _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
BuildContext context) async {
final List> menuItems = [
_connectAction(context),
- if (!isWeb) _transferFileAction(context),
+ _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
}
void _rdpDialog(String id) async {
+ final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port);
@@ -1253,58 +1257,58 @@ void _rdpDialog(String id) async {
hintText: '3389'),
controller: portController,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
- Row(
- children: [
- (isDesktop || isWebDesktop)
- ? ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 140),
- child: Text(
- "${translate('Username')}:",
- textAlign: TextAlign.right,
- ).marginOnly(right: 10))
- : SizedBox.shrink(),
- Expanded(
- child: TextField(
- decoration: InputDecoration(
- labelText: (isDesktop || isWebDesktop)
- ? null
- : translate('Username')),
- controller: userController,
- ),
- ),
- ],
- ).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
- Row(
- children: [
- (isDesktop || isWebDesktop)
- ? ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 140),
- child: Text(
- "${translate('Password')}:",
- textAlign: TextAlign.right,
- ).marginOnly(right: 10))
- : SizedBox.shrink(),
- Expanded(
- child: Obx(() => TextField(
- obscureText: secure.value,
+ Obx(() => Row(
+ children: [
+ stateGlobal.isPortrait.isFalse
+ ? ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ "${translate('Username')}:",
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10))
+ : SizedBox.shrink(),
+ Expanded(
+ child: TextField(
decoration: InputDecoration(
- labelText: (isDesktop || isWebDesktop)
- ? null
- : translate('Password'),
- suffixIcon: IconButton(
- onPressed: () => secure.value = !secure.value,
- icon: Icon(secure.value
- ? Icons.visibility_off
- : Icons.visibility))),
- controller: passwordController,
- )),
- ),
- ],
- )
+ labelText:
+ isDesktop ? null : translate('Username')),
+ controller: userController,
+ ).workaroundFreezeLinuxMint(),
+ ),
+ ],
+ ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
+ Obx(() => Row(
+ children: [
+ stateGlobal.isPortrait.isFalse
+ ? ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ "${translate('Password')}:",
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10))
+ : SizedBox.shrink(),
+ Expanded(
+ child: Obx(() => TextField(
+ obscureText: secure.value,
+ maxLength: maxLength,
+ decoration: InputDecoration(
+ labelText:
+ isDesktop ? null : translate('Password'),
+ suffixIcon: IconButton(
+ onPressed: () =>
+ secure.value = !secure.value,
+ icon: Icon(secure.value
+ ? Icons.visibility_off
+ : Icons.visibility))),
+ controller: passwordController,
+ ).workaroundFreezeLinuxMint()),
+ ),
+ ],
+ ))
],
),
),
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index 8fe73144999f..9d21ec6cd71d 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -107,33 +108,33 @@ class _PeerTabPageState extends State
Widget build(BuildContext context) {
final model = Provider.of(context);
Widget selectionWrap(Widget widget) {
- return model.multiSelectionMode ? createMultiSelectionBar() : widget;
+ return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
}
return Column(
textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- SizedBox(
- height: 32,
- child: Container(
- padding: (isDesktop || isWebDesktop)
- ? null
- : EdgeInsets.symmetric(horizontal: 2),
- child: selectionWrap(Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Expanded(
- child:
- visibleContextMenuListener(_createSwitchBar(context))),
- if (isMobile)
- ..._mobileRightActions(context)
- else
- ..._desktopRightActions(context)
- ],
- )),
- ),
- ).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
+ Obx(() => SizedBox(
+ height: 32,
+ child: Container(
+ padding: stateGlobal.isPortrait.isTrue
+ ? EdgeInsets.symmetric(horizontal: 2)
+ : null,
+ child: selectionWrap(Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: visibleContextMenuListener(
+ _createSwitchBar(context))),
+ if (stateGlobal.isPortrait.isTrue)
+ ..._portraitRightActions(context)
+ else
+ ..._landscapeRightActions(context)
+ ],
+ )),
+ ),
+ ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(),
],
);
@@ -299,7 +300,7 @@ class _PeerTabPageState extends State
}
Widget visibleContextMenuListener(Widget child) {
- if (isMobile) {
+ if (!(isDesktop || isWebDesktop)) {
return GestureDetector(
onLongPressDown: (e) {
final x = e.globalPosition.dx;
@@ -361,8 +362,7 @@ class _PeerTabPageState extends State
.toList());
}
- Widget createMultiSelectionBar() {
- final model = Provider.of(context);
+ Widget createMultiSelectionBar(PeerTabModel model) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -380,7 +380,7 @@ class _PeerTabPageState extends State
Row(
children: [
selectionCount(model.selectedPeers.length),
- selectAll(),
+ selectAll(model),
closeSelection(),
],
)
@@ -456,7 +456,7 @@ class _PeerTabPageState extends State
showToast(translate('Successful'));
},
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
- ).marginOnly(left: isMobile ? 11 : 6),
+ ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -477,7 +477,7 @@ class _PeerTabPageState extends State
model.setMultiSelectionMode(false);
},
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
- ).marginOnly(left: isMobile ? 11 : 6),
+ ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -500,7 +500,7 @@ class _PeerTabPageState extends State
});
},
child: Icon(Icons.tag))
- .marginOnly(left: isMobile ? 11 : 6),
+ .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -511,8 +511,7 @@ class _PeerTabPageState extends State
);
}
- Widget selectAll() {
- final model = Provider.of(context);
+ Widget selectAll(PeerTabModel model) {
return Offstage(
offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length,
@@ -556,10 +555,10 @@ class _PeerTabPageState extends State
});
}
- List _desktopRightActions(BuildContext context) {
+ List _landscapeRightActions(BuildContext context) {
final model = Provider.of(context);
return [
- const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
+ const PeerSearchBar().marginOnly(right: 13),
_createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh(
@@ -580,7 +579,7 @@ class _PeerTabPageState extends State
];
}
- List _mobileRightActions(BuildContext context) {
+ List _portraitRightActions(BuildContext context) {
final model = Provider.of(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@@ -701,13 +700,13 @@ class _PeerSearchBarState extends State {
baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length);
});
- return Container(
- width: isMobile ? 120 : 140,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(6),
- ),
- child: Obx(() => Row(
+ return Obx(() => Container(
+ width: stateGlobal.isPortrait.isTrue ? 120 : 140,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Row(
children: [
Expanded(
child: Row(
@@ -744,7 +743,7 @@ class _PeerSearchBarState extends State {
border: InputBorder.none,
isDense: true,
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
// Icon(Icons.close),
IconButton(
@@ -768,8 +767,8 @@ class _PeerSearchBarState extends State {
),
)
],
- )),
- );
+ ),
+ ));
}
}
diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart
index ef9647eaa93d..3e34f882d1de 100644
--- a/flutter/lib/common/widgets/peers_view.dart
+++ b/flutter/lib/common/widgets/peers_view.dart
@@ -5,7 +5,9 @@ import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
-import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
+import 'package:flutter_hbb/models/ab_model.dart';
+import 'package:flutter_hbb/models/peer_tab_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -41,6 +43,14 @@ class LoadEvent {
static const String group = 'load_group_peers';
}
+class PeersModelName {
+ static const String recent = 'recent peer';
+ static const String favorite = 'fav peer';
+ static const String lan = 'discovered peer';
+ static const String addressBook = 'address book peer';
+ static const String group = 'group peer';
+}
+
/// for peer search text, global obs value
final peerSearchText = "".obs;
@@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView>
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = {};
var _lastQueryTime = DateTime.now();
+ var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0;
var _exit = false;
bool _isActive = true;
@@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView>
@override
void onWindowFocus() {
_queryCount = 0;
+ _isActive = true;
}
@override
- void onWindowMinimize() {
+ void onWindowBlur() {
+ // We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
+ // Maybe it's a bug of the window manager, but the source code seems to be correct.
+ //
+ // Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
+ // we need the following comparison to ensure that `_isActive` is true in the end.
+ if (isWindows &&
+ DateTime.now().difference(_lastWindowRestoreTime) <
+ const Duration(milliseconds: 300)) {
+ return;
+ }
_queryCount = _maxQueryCount;
+ _isActive = false;
+ }
+
+ @override
+ void onWindowRestore() {
+ // Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
+ // But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
+ if (!isWindows) return;
+ _queryCount = 0;
+ _isActive = true;
+ _lastWindowRestoreTime = DateTime.now();
+ }
+
+ @override
+ void onWindowMinimize() {
+ // Window minimize also triggers `onWindowBlur()`.
}
// This function is required for mobile.
@@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
- if (isDesktop) return;
+ if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) {
_isActive = true;
_queryCount = 0;
@@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView>
@override
Widget build(BuildContext context) {
- return ChangeNotifierProvider(
- create: (context) => widget.peers,
+ // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
+ // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
+ // Simple demo can reproduce this issue.
+ return ChangeNotifierProvider.value(
+ value: widget.peers,
child: Consumer(builder: (context, peers, child) {
if (peers.peers.isEmpty) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
@@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView>
var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
- buildOnePeer(Peer peer) {
+ buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged,
@@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView>
// No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView().
- return (isDesktop || isWebDesktop)
+ return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid
@@ -217,44 +258,36 @@ class _PeersViewState extends State<_PeersView>
: Container(child: visibilityChild);
}
- final Widget child;
- if (isMobile) {
- child = ListView.builder(
- itemCount: peers.length,
- itemBuilder: (BuildContext context, int index) {
- return buildOnePeer(peers[index]).marginOnly(
- top: index == 0 ? 0 : space / 2, bottom: space / 2);
- },
- );
- } else {
- child = Obx(() => peerCardUiType.value == PeerUiType.list
- ? DesktopScrollWrapper(
- scrollController: _scrollController,
- child: ListView.builder(
- controller: _scrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- itemCount: peers.length,
- itemBuilder: (BuildContext context, int index) {
- return buildOnePeer(peers[index]).marginOnly(
- right: space,
- top: index == 0 ? 0 : space / 2,
- bottom: space / 2);
- }),
- )
- : DesktopScrollWrapper(
- scrollController: _scrollController,
- child: DynamicGridView.builder(
- controller: _scrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- gridDelegate: SliverGridDelegateWithWrapping(
- mainAxisSpacing: space / 2,
- crossAxisSpacing: space),
- itemCount: peers.length,
- itemBuilder: (BuildContext context, int index) {
- return buildOnePeer(peers[index]);
- }),
- ));
- }
+ // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
+ // Continious rebuilds of `ListView.builder` will cause memory leak.
+ // Simple demo can reproduce this issue.
+ final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
+ ? ListView.builder(
+ itemCount: peers.length,
+ itemBuilder: (BuildContext context, int index) {
+ return buildOnePeer(peers[index], true).marginOnly(
+ top: index == 0 ? 0 : space / 2, bottom: space / 2);
+ },
+ )
+ : peerCardUiType.value == PeerUiType.list
+ ? ListView.builder(
+ controller: _scrollController,
+ itemCount: peers.length,
+ itemBuilder: (BuildContext context, int index) {
+ return buildOnePeer(peers[index], false).marginOnly(
+ right: space,
+ top: index == 0 ? 0 : space / 2,
+ bottom: space / 2);
+ },
+ )
+ : DynamicGridView.builder(
+ gridDelegate: SliverGridDelegateWithWrapping(
+ mainAxisSpacing: space / 2,
+ crossAxisSpacing: space),
+ itemCount: peers.length,
+ itemBuilder: (BuildContext context, int index) {
+ return buildOnePeer(peers[index], false);
+ }));
if (updateEvent == UpdateEvent.load) {
_curPeers.clear();
@@ -290,7 +323,12 @@ class _PeersViewState extends State<_PeersView>
_queryOnlines(false);
}
} else {
- if (_isActive && (_queryCount < _maxQueryCount || !p)) {
+ final skipIfIsWeb =
+ isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
+ final skipIfMobile =
+ (isAndroid || isIOS) && !stateGlobal.isInMainPage;
+ final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
+ if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -371,28 +409,39 @@ class _PeersViewState extends State<_PeersView>
}
abstract class BasePeersView extends StatelessWidget {
- final String name;
- final String loadEvent;
+ final PeerTabIndex peerTabIndex;
final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder;
- final GetInitPeers? getInitPeers;
const BasePeersView({
Key? key,
- required this.name,
- required this.loadEvent,
+ required this.peerTabIndex,
this.peerFilter,
required this.peerCardBuilder,
- required this.getInitPeers,
}) : super(key: key);
@override
Widget build(BuildContext context) {
+ Peers peers;
+ switch (peerTabIndex) {
+ case PeerTabIndex.recent:
+ peers = gFFI.recentPeersModel;
+ break;
+ case PeerTabIndex.fav:
+ peers = gFFI.favoritePeersModel;
+ break;
+ case PeerTabIndex.lan:
+ peers = gFFI.lanPeersModel;
+ break;
+ case PeerTabIndex.ab:
+ peers = gFFI.abModel.peersModel;
+ break;
+ case PeerTabIndex.group:
+ peers = gFFI.groupModel.peersModel;
+ break;
+ }
return _PeersView(
- peers:
- Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
- peerFilter: peerFilter,
- peerCardBuilder: peerCardBuilder);
+ peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
}
}
@@ -401,13 +450,11 @@ class RecentPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super(
key: key,
- name: 'recent peer',
- loadEvent: LoadEvent.recent,
+ peerTabIndex: PeerTabIndex.recent,
peerCardBuilder: (Peer peer) => RecentPeerCard(
peer: peer,
menuPadding: menuPadding,
),
- getInitPeers: null,
);
@override
@@ -423,13 +470,11 @@ class FavoritePeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super(
key: key,
- name: 'favorite peer',
- loadEvent: LoadEvent.favorite,
+ peerTabIndex: PeerTabIndex.fav,
peerCardBuilder: (Peer peer) => FavoritePeerCard(
peer: peer,
menuPadding: menuPadding,
),
- getInitPeers: null,
);
@override
@@ -445,13 +490,11 @@ class DiscoveredPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super(
key: key,
- name: 'discovered peer',
- loadEvent: LoadEvent.lan,
+ peerTabIndex: PeerTabIndex.lan,
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
peer: peer,
menuPadding: menuPadding,
),
- getInitPeers: null,
);
@override
@@ -464,36 +507,38 @@ class DiscoveredPeersView extends BasePeersView {
class AddressBookPeersView extends BasePeersView {
AddressBookPeersView(
- {Key? key,
- EdgeInsets? menuPadding,
- ScrollController? scrollController,
- required GetInitPeers getInitPeers})
+ {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super(
key: key,
- name: 'address book peer',
- loadEvent: LoadEvent.addressBook,
+ peerTabIndex: PeerTabIndex.ab,
peerFilter: (Peer peer) =>
_hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
peer: peer,
menuPadding: menuPadding,
),
- getInitPeers: getInitPeers,
);
static bool _hitTag(List selectedTags, List idents) {
if (selectedTags.isEmpty) {
return true;
}
+ // The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations.
+ final selectedNormalTags =
+ selectedTags.where((tag) => tag != kUntagged).toList();
+ if (selectedTags.contains(kUntagged)) {
+ if (idents.isEmpty) return true;
+ if (selectedNormalTags.isEmpty) return false;
+ }
if (gFFI.abModel.filterByIntersection.value) {
- for (final tag in selectedTags) {
+ for (final tag in selectedNormalTags) {
if (!idents.contains(tag)) {
return false;
}
}
return true;
} else {
- for (final tag in selectedTags) {
+ for (final tag in selectedNormalTags) {
if (idents.contains(tag)) {
return true;
}
@@ -505,20 +550,15 @@ class AddressBookPeersView extends BasePeersView {
class MyGroupPeerView extends BasePeersView {
MyGroupPeerView(
- {Key? key,
- EdgeInsets? menuPadding,
- ScrollController? scrollController,
- required GetInitPeers getInitPeers})
+ {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super(
key: key,
- name: 'group peer',
- loadEvent: LoadEvent.group,
+ peerTabIndex: PeerTabIndex.group,
peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer,
menuPadding: menuPadding,
),
- getInitPeers: getInitPeers,
);
static bool filter(Peer peer) {
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index 61bd4dd31bc5..6eb9b0594708 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ // https://github.com/flutter/flutter/issues/154053
+ final useRawKeyEvents = isLinux && !isWeb;
+ // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
+ // while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope(
autofocus: true,
child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
- onKey: (FocusNode data, RawKeyEvent e) =>
- inputModel.handleRawKeyEvent(e),
+ onKey: useRawKeyEvents
+ ? (FocusNode data, RawKeyEvent event) =>
+ inputModel.handleRawKeyEvent(event)
+ : null,
+ onKeyEvent: useRawKeyEvents
+ ? null
+ : (FocusNode node, KeyEvent event) =>
+ inputModel.handleKeyEvent(event),
child: child));
}
}
@@ -74,8 +84,17 @@ class _RawTouchGestureDetectorRegionState
double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1;
+ // Workaround tap down event when two fingers are used to scale(mobile)
+ TapDownDetails? _lastTapDownDetails;
+
PointerDeviceKind? lastDeviceKind;
+ // For touch mode, onDoubleTap
+ // `onDoubleTap()` does not provide the position of the tap event.
+ Offset _lastPosOfDoubleTapDown = Offset.zero;
+ bool _touchModePanStarted = false;
+ Offset _doubleFinerTapPosition = Offset.zero;
+
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel;
@@ -90,152 +109,190 @@ class _RawTouchGestureDetectorRegionState
);
}
- onTapDown(TapDownDetails d) {
+ onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
+ _lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode"
- if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
- inputModel.tapDown(MouseButtons.left);
- }
+ _lastTapDownDetails = d;
}
}
- onTapUp(TapUpDetails d) {
+ onTapUp(TapUpDetails d) async {
+ final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
+ _lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
- if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
- inputModel.tapUp(MouseButtons.left);
+ final isMoved =
+ await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ if (isMoved) {
+ if (lastTapDownDetails != null) {
+ await inputModel.tapDown(MouseButtons.left);
+ }
+ await inputModel.tapUp(MouseButtons.left);
}
}
}
- onTap() {
+ onTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
// Mobile, "Mouse mode"
- inputModel.tap(MouseButtons.left);
+ await inputModel.tap(MouseButtons.left);
}
}
- onDoubleTapDown(TapDownDetails d) {
+ onDoubleTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
- ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ _lastPosOfDoubleTapDown = d.localPosition;
+ await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
}
}
- onDoubleTap() {
+ onDoubleTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return;
}
- inputModel.tap(MouseButtons.left);
- inputModel.tap(MouseButtons.left);
+ if (handleTouch &&
+ !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
+ return;
+ }
+ await inputModel.tap(MouseButtons.left);
+ await inputModel.tap(MouseButtons.left);
}
- onLongPressDown(LongPressDownDetails d) {
+ onLongPressDown(LongPressDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
- ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ _lastPosOfDoubleTapDown = d.localPosition;
_cacheLongPressPosition = d.localPosition;
+ if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
+ return;
+ }
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
}
}
- onLongPressUp() {
+ onLongPressUp() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
- inputModel.tapUp(MouseButtons.left);
+ await inputModel.tapUp(MouseButtons.left);
}
}
// for mobiles
- onLongPress() {
+ onLongPress() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
- ffi.cursorModel
+ final isMoved = await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+ if (!isMoved) {
+ return;
+ }
+ }
+ if (!ffi.ffiModel.isPeerMobile) {
+ await inputModel.tap(MouseButtons.right);
}
- inputModel.tap(MouseButtons.right);
}
- onDoubleFinerTapDown(TapDownDetails d) {
+ onDoubleFinerTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
+ _doubleFinerTapPosition = d.localPosition;
// ignore for desktop and mobile
}
- onDoubleFinerTap(TapDownDetails d) {
+ onDoubleFinerTap(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
- if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
- inputModel.tap(MouseButtons.right);
+
+ // mobile mouse mode or desktop touch screen
+ final isMobileMouseMode = isMobile && !ffiModel.touchMode;
+ // We can't use `d.localPosition` here because it's always (0, 0) on desktop.
+ final isDesktopInRemoteRect = (isDesktop || isWebDesktop) &&
+ ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition);
+ if (isMobileMouseMode || isDesktopInRemoteRect) {
+ await inputModel.tap(MouseButtons.right);
}
}
- onHoldDragStart(DragStartDetails d) {
+ onHoldDragStart(DragStartDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
- inputModel.sendMouse('down', MouseButtons.left);
+ await inputModel.sendMouse('down', MouseButtons.left);
}
}
- onHoldDragUpdate(DragUpdateDetails d) {
+ onHoldDragUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
- ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
+ await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
}
- onHoldDragEnd(DragEndDetails d) {
+ onHoldDragEnd(DragEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
- inputModel.sendMouse('up', MouseButtons.left);
+ await inputModel.sendMouse('up', MouseButtons.left);
}
}
- onOneFingerPanStart(BuildContext context, DragStartDetails d) {
+ onOneFingerPanStart(BuildContext context, DragStartDetails d) async {
+ final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
+ _lastTapDownDetails = null;
lastDeviceKind = d.kind ?? lastDeviceKind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
+ if (lastTapDownDetails != null) {
+ await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
+ lastTapDownDetails.localPosition.dy);
+ }
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
- if (isDesktop) {
+ if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
+ return;
+ }
+
+ _touchModePanStarted = true;
+ if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
+
// Workaround for the issue that the first pan event is sent a long time after the start event.
// If the time interval between the start event and the first pan event is less than 500ms,
// we consider to use the long press position as the start position.
@@ -243,11 +300,11 @@ class _RawTouchGestureDetectorRegionState
// TODO: We should find a better way to send the first pan event as soon as possible.
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
500) {
- ffi.cursorModel
+ await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
- inputModel.sendMouse('down', MouseButtons.left);
- ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ await inputModel.sendMouse('down', MouseButtons.left);
+ await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
final offset = ffi.cursorModel.offset;
final cursorX = offset.dx;
@@ -256,39 +313,46 @@ class _RawTouchGestureDetectorRegionState
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
final size = MediaQueryData.fromView(View.of(context)).size;
if (!visible.contains(Offset(cursorX, cursorY))) {
- ffi.cursorModel.move(size.width / 2, size.height / 2);
+ await ffi.cursorModel.move(size.width / 2, size.height / 2);
}
}
}
- onOneFingerPanUpdate(DragUpdateDetails d) {
+ onOneFingerPanUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
- ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
+ if (handleTouch && !_touchModePanStarted) {
+ return;
+ }
+ await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
- onOneFingerPanEnd(DragEndDetails d) {
+ onOneFingerPanEnd(DragEndDetails d) async {
+ _touchModePanStarted = false;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
- if (isDesktop) {
+ if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
- inputModel.sendMouse('up', MouseButtons.left);
+ if (handleTouch) {
+ await inputModel.sendMouse('up', MouseButtons.left);
+ }
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
+ _lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
}
- onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
+ onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
@@ -297,7 +361,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale;
if (scale != 0) {
- bind.sessionSendPointer(
+ await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
@@ -312,21 +376,22 @@ class _RawTouchGestureDetectorRegionState
}
}
- onTwoFingerScaleEnd(ScaleEndDetails d) {
+ onTwoFingerScaleEnd(ScaleEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if ((isDesktop || isWebDesktop)) {
- bind.sessionSendPointer(
+ await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
} else {
// mobile
_scale = 1;
- bind.sessionSetViewStyle(sessionId: sessionId, value: "");
+ // No idea why we need to set the view style to "" here.
+ // bind.sessionSetViewStyle(sessionId: sessionId, value: "");
}
- inputModel.sendMouse('up', MouseButtons.left);
+ await inputModel.sendMouse('up', MouseButtons.left);
}
get onHoldDragCancel => null;
diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart
index 0b56d9f4c149..153121057e5e 100644
--- a/flutter/lib/common/widgets/toolbar.dart
+++ b/flutter/lib/common/widgets/toolbar.dart
@@ -147,12 +147,23 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset()));
}
+
+ connectWithToken(
+ {required bool isFileTransfer, required bool isTcpTunneling}) {
+ final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
+ connect(context, id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ connToken: connToken);
+ }
+
// transferFile
if (isDesktop) {
v.add(
TTextMenu(
child: Text(translate('Transfer file')),
- onPressed: () => connect(context, id, isFileTransfer: true)),
+ onPressed: () =>
+ connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
);
}
// tcpTunneling
@@ -160,7 +171,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(
TTextMenu(
child: Text(translate('TCP tunneling')),
- onPressed: () => connect(context, id, isTcpTunneling: true)),
+ onPressed: () =>
+ connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
);
}
// note
@@ -183,7 +195,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
v.add(
TTextMenu(
- child: Text('${translate("Insert")} Ctrl + Alt + Del'),
+ child: Text('${translate("Insert Ctrl + Alt + Del")}'),
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
);
}
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 5af3f6d251fd..95b207826a71 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
+const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0;
@@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
const String kOptionEnableHwcodec = "enable-hwcodec";
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
+const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";
@@ -136,6 +138,7 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
+const String kOptionEnableTrustedDevices = "enable-trusted-devices";
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
@@ -166,6 +169,13 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
+const String kMouseEventTypeDefault = "";
+const String kMouseEventTypePanStart = "pan_start";
+const String kMouseEventTypePanUpdate = "pan_update";
+const String kMouseEventTypePanEnd = "pan_end";
+const String kMouseEventTypeDown = "down";
+const String kMouseEventTypeUp = "up";
+
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows =
@@ -234,15 +244,11 @@ const double kDesktopIconButtonSplashRadius = 20;
/// [kMinCursorSize] indicates min cursor (w, h)
const int kMinCursorSize = 12;
-/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse
-const kDefaultScrollAmountMultiplier = 5.0;
-const kDefaultScrollDuration = Duration(milliseconds: 50);
-const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
-// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
-final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
-final kWindowBorderWidth = 1.0;
+// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
+const kWindowResizeEdgeSize = 5.0;
+const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;
@@ -568,3 +574,5 @@ enum WindowsTarget {
extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this);
}
+
+const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index 1403d4493471..235e2185a063 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -3,8 +3,8 @@
import 'dart:async';
import 'dart:convert';
-import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
@@ -39,7 +39,7 @@ class _OnlineStatusWidgetState extends State {
double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() {
- const url = "https://rustdesk.com/pricing.html";
+ const url = "https://rustdesk.com/pricing";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
@@ -169,20 +169,19 @@ class _OnlineStatusWidgetState extends State {
final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map;
final statusNum = status['status_num'] as int;
- final preStatus = stateGlobal.svcStatus.value;
if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready;
- if (preStatus != SvcStatus.ready) {
- gFFI.userModel.refreshCurrentUser();
- }
} else {
stateGlobal.svcStatus.value = SvcStatus.notReady;
}
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
+ try {
+ stateGlobal.videoConnCount.value = status['video_conn_count'] as int;
+ } catch (_) {}
}
}
@@ -207,6 +206,8 @@ class _ConnectionPageState extends State
bool isPeersLoading = false;
bool isPeersLoaded = false;
+ // https://github.com/flutter/flutter/issues/157244
+ Iterable _autocompleteOpts = [];
@override
void initState() {
@@ -261,8 +262,9 @@ class _ConnectionPageState extends State
@override
void onWindowLeaveFullScreen() {
// Restore edge border to default edge size.
- stateGlobal.resizeEdgeSize.value =
- stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
+ stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
+ ? kMaximizeEdgeSize
+ : windowResizeEdgeSize;
}
@override
@@ -326,43 +328,14 @@ class _ConnectionPageState extends State
child: Ink(
child: Column(
children: [
- Row(
- children: [
- Expanded(
- child: Row(
- children: [
- AutoSizeText(
- translate('Control Remote Desktop'),
- maxLines: 1,
- style: Theme.of(context)
- .textTheme
- .titleLarge
- ?.merge(TextStyle(height: 1)),
- ).marginOnly(right: 4),
- Tooltip(
- waitDuration: Duration(milliseconds: 300),
- message: translate("id_input_tip"),
- child: Icon(
- Icons.help_outline_outlined,
- size: 16,
- color: Theme.of(context)
- .textTheme
- .titleLarge
- ?.color
- ?.withOpacity(0.5),
- ),
- ),
- ],
- )),
- ],
- ).marginOnly(bottom: 15),
+ getConnectionPageTitle(context, false).marginOnly(bottom: 15),
Row(
children: [
Expanded(
child: Autocomplete(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
- return const Iterable.empty();
+ _autocompleteOpts = const Iterable.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
@@ -378,7 +351,7 @@ class _ConnectionPageState extends State
rdpUsername: '',
loginName: '',
);
- return [emptyPeer];
+ _autocompleteOpts = [emptyPeer];
} else {
String textWithoutSpaces =
textEditingValue.text.replaceAll(" ", "");
@@ -389,8 +362,7 @@ class _ConnectionPageState extends State
);
}
String textToFind = textEditingValue.text.toLowerCase();
-
- return peers
+ _autocompleteOpts = peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -402,6 +374,7 @@ class _ConnectionPageState extends State
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
+ return _autocompleteOpts;
},
fieldViewBuilder: (
BuildContext context,
@@ -451,7 +424,7 @@ class _ConnectionPageState extends State
onSubmitted: (_) {
onConnect();
},
- ));
+ ).workaroundFreezeLinuxMint());
},
onSelected: (option) {
setState(() {
@@ -462,6 +435,7 @@ class _ConnectionPageState extends State
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected onSelected,
Iterable options) {
+ options = _autocompleteOpts;
double maxHeight = options.length * 50;
if (options.length == 1) {
maxHeight = 52;
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 31a8e1374ff5..ba724eed5eee 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -12,9 +12,9 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
-import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
@@ -35,12 +35,11 @@ class DesktopHomePage extends StatefulWidget {
const borderColor = Color(0xFF2F65BA);
class _DesktopHomePageState extends State
- with AutomaticKeepAliveClientMixin {
+ with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _leftPaneScrollController = ScrollController();
@override
bool get wantKeepAlive => true;
- var updateUrl = '';
var systemError = '';
StreamSubscription? _uniLinksSubscription;
var svcStopped = false.obs;
@@ -52,6 +51,7 @@ class _DesktopHomePageState extends State
bool isCardClosed = false;
final RxBool _editHover = false.obs;
+ final RxBool _block = false.obs;
final GlobalKey _childKey = GlobalKey();
@@ -59,14 +59,20 @@ class _DesktopHomePageState extends State
Widget build(BuildContext context) {
super.build(context);
final isIncomingOnly = bind.isIncomingOnly();
- return Row(
+ return _buildBlock(
+ child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildLeftPane(context),
if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
],
- );
+ ));
+ }
+
+ Widget _buildBlock({required Widget child}) {
+ return buildRemoteBlock(
+ block: _block, mask: true, use: canBeBlocked, child: child);
}
Widget buildLeftPane(BuildContext context) {
@@ -87,7 +93,8 @@ class _DesktopHomePageState extends State
if (!isOutgoingOnly) buildIDBoard(context),
if (!isOutgoingOnly) buildPasswordBoard(context),
FutureBuilder(
- future: buildHelpCards(),
+ future: Future.value(
+ Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
builder: (_, data) {
if (data.hasData) {
if (isIncomingOnly) {
@@ -125,47 +132,43 @@ class _DesktopHomePageState extends State
child: Container(
width: isIncomingOnly ? 280.0 : 200.0,
color: Theme.of(context).colorScheme.background,
- child: DesktopScrollWrapper(
- scrollController: _leftPaneScrollController,
- child: Stack(
- children: [
- SingleChildScrollView(
- controller: _leftPaneScrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- child: Column(
- key: _childKey,
- children: children,
- ),
+ child: Stack(
+ children: [
+ SingleChildScrollView(
+ controller: _leftPaneScrollController,
+ child: Column(
+ key: _childKey,
+ children: children,
),
- if (isOutgoingOnly)
- Positioned(
- bottom: 6,
- left: 12,
- child: Align(
- alignment: Alignment.centerLeft,
- child: InkWell(
- child: Obx(
- () => Icon(
- Icons.settings,
- color: _editHover.value
- ? textColor
- : Colors.grey.withOpacity(0.5),
- size: 22,
- ),
+ ),
+ if (isOutgoingOnly)
+ Positioned(
+ bottom: 6,
+ left: 12,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: InkWell(
+ child: Obx(
+ () => Icon(
+ Icons.settings,
+ color: _editHover.value
+ ? textColor
+ : Colors.grey.withOpacity(0.5),
+ size: 22,
),
- onTap: () => {
- if (DesktopSettingPage.tabKeys.isNotEmpty)
- {
- DesktopSettingPage.switch2page(
- DesktopSettingPage.tabKeys[0])
- }
- },
- onHover: (value) => _editHover.value = value,
),
+ onTap: () => {
+ if (DesktopSettingPage.tabKeys.isNotEmpty)
+ {
+ DesktopSettingPage.switch2page(
+ DesktopSettingPage.tabKeys[0])
+ }
+ },
+ onHover: (value) => _editHover.value = value,
),
- )
- ],
- ),
+ ),
+ )
+ ],
),
),
);
@@ -234,7 +237,7 @@ class _DesktopHomePageState extends State
style: TextStyle(
fontSize: 22,
),
- ),
+ ).workaroundFreezeLinuxMint(),
),
)
],
@@ -272,10 +275,21 @@ class _DesktopHomePageState extends State
}
buildPasswordBoard(BuildContext context) {
- final model = gFFI.serverModel;
+ return ChangeNotifierProvider.value(
+ value: gFFI.serverModel,
+ child: Consumer(
+ builder: (context, model, child) {
+ return buildPasswordBoard2(context, model);
+ },
+ ));
+ }
+
+ buildPasswordBoard2(BuildContext context, ServerModel model) {
RxBool refreshHover = false.obs;
RxBool editHover = false.obs;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
+ final showOneTime = model.approveMode != 'click' &&
+ model.verificationMethod != kUsePermanentPassword;
return Container(
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
child: Row(
@@ -304,8 +318,7 @@ class _DesktopHomePageState extends State
Expanded(
child: GestureDetector(
onDoubleTap: () {
- if (model.verificationMethod !=
- kUsePermanentPassword) {
+ if (showOneTime) {
Clipboard.setData(
ClipboardData(text: model.serverPasswd.text));
showToast(translate("Copied"));
@@ -320,25 +333,26 @@ class _DesktopHomePageState extends State
EdgeInsets.only(top: 14, bottom: 10),
),
style: TextStyle(fontSize: 15),
- ),
+ ).workaroundFreezeLinuxMint(),
),
),
- AnimatedRotationWidget(
- onPressed: () => bind.mainUpdateTemporaryPassword(),
- child: Tooltip(
- message: translate('Refresh Password'),
- child: Obx(() => RotatedBox(
- quarterTurns: 2,
- child: Icon(
- Icons.refresh,
- color: refreshHover.value
- ? textColor
- : Color(0xFFDDDDDD),
- size: 22,
- ))),
- ),
- onHover: (value) => refreshHover.value = value,
- ).marginOnly(right: 8, top: 4),
+ if (showOneTime)
+ AnimatedRotationWidget(
+ onPressed: () => bind.mainUpdateTemporaryPassword(),
+ child: Tooltip(
+ message: translate('Refresh Password'),
+ child: Obx(() => RotatedBox(
+ quarterTurns: 2,
+ child: Icon(
+ Icons.refresh,
+ color: refreshHover.value
+ ? textColor
+ : Color(0xFFDDDDDD),
+ size: 22,
+ ))),
+ ),
+ onHover: (value) => refreshHover.value = value,
+ ).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings())
InkWell(
child: Tooltip(
@@ -409,14 +423,14 @@ class _DesktopHomePageState extends State
);
}
- Future buildHelpCards() async {
+ Widget buildHelpCards(String updateUrl) {
if (!bind.isCustomClient() &&
updateUrl.isNotEmpty &&
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
return buildInstallCard(
"Status",
- "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
+ "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
@@ -663,12 +677,6 @@ class _DesktopHomePageState extends State
@override
void initState() {
super.initState();
- if (!bind.isCustomClient()) {
- Timer(const Duration(seconds: 1), () async {
- updateUrl = await bind.mainGetSoftwareUpdateUrl();
- if (updateUrl.isNotEmpty) setState(() {});
- });
- }
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID();
final error = await bind.mainGetError();
@@ -766,6 +774,7 @@ class _DesktopHomePageState extends State
isRDP: call.arguments['isRDP'],
password: call.arguments['password'],
forceRelay: call.arguments['forceRelay'],
+ connToken: call.arguments['connToken'],
);
} else if (call.method == kWindowEventMoveTabToNewWindow) {
final args = call.arguments.split(',');
@@ -803,6 +812,7 @@ class _DesktopHomePageState extends State
_updateWindowSize();
});
}
+ WidgetsBinding.instance.addObserver(this);
}
_updateWindowSize() {
@@ -824,9 +834,18 @@ class _DesktopHomePageState extends State
_uniLinksSubscription?.cancel();
Get.delete(tag: 'stop-service');
_updateTimer?.cancel();
+ WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ if (state == AppLifecycleState.resumed) {
+ shouldBeBlocked(_block, canBeBlocked);
+ }
+ }
+
Widget buildPluginEntry() {
final entries = PluginUiManager.instance.entries.entries;
return Offstage(
@@ -857,6 +876,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
// SpecialCharacterValidationRule(),
MinCharactersValidationRule(8),
];
+ final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) {
submit() {
@@ -915,7 +935,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg0 = '';
});
},
- ),
+ maxLength: maxLength,
+ ).workaroundFreezeLinuxMint(),
),
],
),
@@ -941,7 +962,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg1 = '';
});
},
- ),
+ maxLength: maxLength,
+ ).workaroundFreezeLinuxMint(),
),
],
),
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index 79c18c521183..f89381a3ff5b 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -11,15 +11,16 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
+import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
-import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
@@ -61,7 +62,8 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List tabKeys = [
SettingsTabKey.general,
- if (!bind.isOutgoingOnly() &&
+ if (!isWeb &&
+ !bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety,
@@ -105,13 +107,20 @@ class DesktopSettingPage extends StatefulWidget {
}
class _DesktopSettingPageState extends State
- with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
+ with
+ TickerProviderStateMixin,
+ AutomaticKeepAliveClientMixin,
+ WidgetsBindingObserver {
late PageController controller;
late Rx selectedTab;
@override
bool get wantKeepAlive => true;
+ final RxBool _block = false.obs;
+ final RxBool _canBeBlocked = false.obs;
+ Timer? _videoConnTimer;
+
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
if (initialIndex == -1) {
@@ -131,11 +140,34 @@ class _DesktopSettingPageState extends State
});
}
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ if (state == AppLifecycleState.resumed) {
+ shouldBeBlocked(_block, canBeBlocked);
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ _videoConnTimer =
+ periodic_immediate(Duration(milliseconds: 1000), () async {
+ if (!mounted) {
+ return;
+ }
+ _canBeBlocked.value = await canBeBlocked();
+ });
+ }
+
@override
void dispose() {
super.dispose();
Get.delete(tag: _kSettingPageControllerTag);
Get.delete(tag: _kSettingPageTabKeyTag);
+ WidgetsBinding.instance.removeObserver(this);
+ _videoConnTimer?.cancel();
}
List<_TabInfo> _settingTabs() {
@@ -205,18 +237,41 @@ class _DesktopSettingPageState extends State
return children;
}
+ Widget _buildBlock({required List children}) {
+ // check both mouseMoveTime and videoConnCount
+ return Obx(() {
+ final videoConnBlock =
+ _canBeBlocked.value && stateGlobal.videoConnCount > 0;
+ return Stack(children: [
+ buildRemoteBlock(
+ block: _block,
+ mask: false,
+ use: canBeBlocked,
+ child: preventMouseKeyBuilder(
+ child: Row(children: children),
+ block: videoConnBlock,
+ ),
+ ),
+ if (videoConnBlock)
+ Container(
+ color: Colors.black.withOpacity(0.5),
+ )
+ ]);
+ });
+ }
+
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
- body: Row(
+ body: _buildBlock(
children: [
SizedBox(
width: _kTabWidth,
child: Column(
children: [
- _header(),
+ _header(context),
Flexible(child: _listView(tabs: _settingTabs())),
],
),
@@ -225,13 +280,11 @@ class _DesktopSettingPageState extends State
Expanded(
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
- child: DesktopScrollWrapper(
- scrollController: controller,
- child: PageView(
- controller: controller,
- physics: NeverScrollableScrollPhysics(),
- children: _children(),
- )),
+ child: PageView(
+ controller: controller,
+ physics: NeverScrollableScrollPhysics(),
+ children: _children(),
+ ),
),
)
],
@@ -239,21 +292,40 @@ class _DesktopSettingPageState extends State
);
}
- Widget _header() {
+ Widget _header(BuildContext context) {
+ final settingsText = Text(
+ translate('Settings'),
+ textAlign: TextAlign.left,
+ style: const TextStyle(
+ color: _accentColor,
+ fontSize: _kTitleFontSize,
+ fontWeight: FontWeight.w400,
+ ),
+ );
return Row(
children: [
- SizedBox(
- height: 62,
- child: Text(
- translate('Settings'),
- textAlign: TextAlign.left,
- style: const TextStyle(
- color: _accentColor,
- fontSize: _kTitleFontSize,
- fontWeight: FontWeight.w400,
+ if (isWeb)
+ IconButton(
+ onPressed: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ }
+ },
+ icon: Icon(Icons.arrow_back),
+ ).marginOnly(left: 5),
+ if (isWeb)
+ SizedBox(
+ height: 62,
+ child: Align(
+ alignment: Alignment.center,
+ child: settingsText,
),
- ),
- ).marginOnly(left: 20, top: 10),
+ ).marginOnly(left: 20),
+ if (!isWeb)
+ SizedBox(
+ height: 62,
+ child: settingsText,
+ ).marginOnly(left: 20, top: 10),
const Spacer(),
],
);
@@ -261,13 +333,10 @@ class _DesktopSettingPageState extends State
Widget _listView({required List<_TabInfo> tabs}) {
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ListView(
- physics: DraggableNeverScrollableScrollPhysics(),
- controller: scrollController,
- children: tabs.map((tab) => _listItem(tab: tab)).toList(),
- ));
+ return ListView(
+ controller: scrollController,
+ children: tabs.map((tab) => _listItem(tab: tab)).toList(),
+ );
}
Widget _listItem({required _TabInfo tab}) {
@@ -322,34 +391,32 @@ class _General extends StatefulWidget {
}
class _GeneralState extends State<_General> {
- final RxBool serviceStop = Get.find(tag: 'stop-service');
+ final RxBool serviceStop =
+ isWeb ? RxBool(false) : Get.find(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs;
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ListView(
- physics: DraggableNeverScrollableScrollPhysics(),
- controller: scrollController,
- children: [
- service(),
- theme(),
- _Card(title: 'Language', children: [language()]),
- hwcodec(),
- audio(context),
- record(context),
- WaylandCard(),
- other()
- ],
- ).marginOnly(bottom: _kListViewBottomMargin));
+ return ListView(
+ controller: scrollController,
+ children: [
+ if (!isWeb) service(),
+ theme(),
+ _Card(title: 'Language', children: [language()]),
+ if (!isWeb) hwcodec(),
+ if (!isWeb) audio(context),
+ if (!isWeb) record(context),
+ if (!isWeb) WaylandCard(),
+ other()
+ ],
+ ).marginOnly(bottom: _kListViewBottomMargin);
}
Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString();
- onChanged(String value) {
- MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
+ onChanged(String value) async {
+ await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
setState(() {});
}
@@ -394,13 +461,13 @@ class _GeneralState extends State<_General> {
Widget other() {
final children = [
- if (!bind.isIncomingOnly())
+ if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
- wallpaper(),
- if (!bind.isIncomingOnly()) ...[
+ if (!isWeb) wallpaper(),
+ if (!isWeb && !bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Open connection in new tab',
@@ -417,18 +484,19 @@ class _GeneralState extends State<_General> {
kOptionAllowAlwaysSoftwareRender,
),
),
- Tooltip(
- message: translate('texture_render_tip'),
- child: _OptionCheckBox(
- context,
- "Use texture rendering",
- kOptionTextureRender,
- optGetter: bind.mainGetUseTextureRender,
- optSetter: (k, v) async =>
- await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
+ if (!isWeb)
+ Tooltip(
+ message: translate('texture_render_tip'),
+ child: _OptionCheckBox(
+ context,
+ "Use texture rendering",
+ kOptionTextureRender,
+ optGetter: bind.mainGetUseTextureRender,
+ optSetter: (k, v) async =>
+ await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
+ ),
),
- ),
- if (!bind.isCustomClient())
+ if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
'Check for software update on startup',
@@ -443,7 +511,7 @@ class _GeneralState extends State<_General> {
)
],
];
- if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
+ if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
}
@@ -515,16 +583,16 @@ class _GeneralState extends State<_General> {
}
builder(devices, currentDevice, setDevice) {
- return _Card(title: 'Audio Input Device', children: [
- ...devices.map((device) => _Radio(context,
- value: device,
- groupValue: currentDevice,
- autoNewLine: false,
- label: device, onChanged: (value) {
- setDevice(value);
- setState(() {});
- }))
- ]);
+ final child = ComboBox(
+ keys: devices,
+ values: devices,
+ initialKey: currentDevice,
+ onChanged: (key) async {
+ setDevice(key);
+ setState(() {});
+ },
+ ).marginOnly(left: _kContentHMargin);
+ return _Card(title: 'Audio Input Device', children: [child]);
}
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
@@ -539,7 +607,6 @@ class _GeneralState extends State<_General> {
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
- // canLaunchUrl blocked on windows portable, user SYSTEM
return {
'user_dir': user_dir,
'root_dir': root_dir,
@@ -553,12 +620,18 @@ class _GeneralState extends State<_General> {
bool root_dir_exists = map['root_dir_exists']!;
bool user_dir_exists = map['user_dir_exists']!;
return _Card(title: 'Recording', children: [
- _OptionCheckBox(context, 'Automatically record incoming sessions',
- kOptionAllowAutoRecordIncoming),
- if (showRootDir)
+ if (!bind.isOutgoingOnly())
+ _OptionCheckBox(context, 'Automatically record incoming sessions',
+ kOptionAllowAutoRecordIncoming),
+ if (!bind.isIncomingOnly())
+ _OptionCheckBox(context, 'Automatically record outgoing sessions',
+ kOptionAllowAutoRecordOutgoing,
+ isServer: false),
+ if (showRootDir && !bind.isOutgoingOnly())
Row(
children: [
- Text('${translate("Incoming")}:'),
+ Text(
+ '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
Expanded(
child: GestureDetector(
onTap: root_dir_exists
@@ -575,45 +648,49 @@ class _GeneralState extends State<_General> {
),
],
).marginOnly(left: _kContentHMargin),
- Row(
- children: [
- Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
- Expanded(
- child: GestureDetector(
- onTap: user_dir_exists
- ? () => launchUrl(Uri.file(user_dir))
- : null,
- child: Text(
- user_dir,
- softWrap: true,
- style: user_dir_exists
- ? const TextStyle(decoration: TextDecoration.underline)
+ if (!(showRootDir && bind.isIncomingOnly()))
+ Row(
+ children: [
+ Text(
+ '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
+ Expanded(
+ child: GestureDetector(
+ onTap: user_dir_exists
+ ? () => launchUrl(Uri.file(user_dir))
: null,
- )).marginOnly(left: 10),
- ),
- ElevatedButton(
- onPressed: isOptionFixed(kOptionVideoSaveDirectory)
- ? null
- : () async {
- String? initialDirectory;
- if (await Directory.fromUri(Uri.directory(user_dir))
- .exists()) {
- initialDirectory = user_dir;
- }
- String? selectedDirectory =
- await FilePicker.platform.getDirectoryPath(
- initialDirectory: initialDirectory);
- if (selectedDirectory != null) {
- await bind.mainSetOption(
- key: kOptionVideoSaveDirectory,
- value: selectedDirectory);
- setState(() {});
- }
- },
- child: Text(translate('Change')))
- .marginOnly(left: 5),
- ],
- ).marginOnly(left: _kContentHMargin),
+ child: Text(
+ user_dir,
+ softWrap: true,
+ style: user_dir_exists
+ ? const TextStyle(
+ decoration: TextDecoration.underline)
+ : null,
+ )).marginOnly(left: 10),
+ ),
+ ElevatedButton(
+ onPressed: isOptionFixed(kOptionVideoSaveDirectory)
+ ? null
+ : () async {
+ String? initialDirectory;
+ if (await Directory.fromUri(
+ Uri.directory(user_dir))
+ .exists()) {
+ initialDirectory = user_dir;
+ }
+ String? selectedDirectory =
+ await FilePicker.platform.getDirectoryPath(
+ initialDirectory: initialDirectory);
+ if (selectedDirectory != null) {
+ await bind.mainSetLocalOption(
+ key: kOptionVideoSaveDirectory,
+ value: selectedDirectory);
+ setState(() {});
+ }
+ },
+ child: Text(translate('Change')))
+ .marginOnly(left: 5),
+ ],
+ ).marginOnly(left: _kContentHMargin),
]);
});
}
@@ -641,8 +718,9 @@ class _GeneralState extends State<_General> {
initialKey: currentKey,
onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
- reloadAllWindows();
- bind.mainChangeLanguage(lang: key);
+ if (isWeb) reloadCurrentWindow();
+ if (!isWeb) reloadAllWindows();
+ if (!isWeb) bind.mainChangeLanguage(lang: key);
},
enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin);
@@ -672,29 +750,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: SingleChildScrollView(
- physics: DraggableNeverScrollableScrollPhysics(),
- controller: scrollController,
- child: Column(
- children: [
- _lock(locked, 'Unlock Security Settings', () {
- locked = false;
- setState(() => {});
- }),
- AbsorbPointer(
- absorbing: locked,
- child: Column(children: [
- permissions(context),
- password(context),
- _Card(title: '2FA', children: [tfa()]),
- _Card(title: 'ID', children: [changeId()]),
- more(context),
- ]),
- ),
- ],
- )).marginOnly(bottom: _kListViewBottomMargin));
+ return SingleChildScrollView(
+ controller: scrollController,
+ child: Column(
+ children: [
+ _lock(locked, 'Unlock Security Settings', () {
+ locked = false;
+ setState(() => {});
+ }),
+ preventMouseKeyBuilder(
+ block: locked,
+ child: Column(children: [
+ permissions(context),
+ password(context),
+ _Card(title: '2FA', children: [tfa()]),
+ _Card(title: 'ID', children: [changeId()]),
+ more(context),
+ ]),
+ ),
+ ],
+ )).marginOnly(bottom: _kListViewBottomMargin);
}
Widget tfa() {
@@ -783,8 +858,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChangedBot(!hasBot.value);
},
).marginOnly(left: _kCheckBoxLeftMargin + 30);
+
+ final trust = Row(
+ children: [
+ Flexible(
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 300),
+ message: translate("enable-trusted-devices-tip"),
+ child: _OptionCheckBox(context, "Enable trusted devices",
+ kOptionEnableTrustedDevices,
+ enabled: !locked, update: (v) {
+ setState(() {});
+ }),
+ ),
+ ),
+ if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
+ ElevatedButton(
+ onPressed: locked
+ ? null
+ : () {
+ manageTrustedDeviceDialog();
+ },
+ child: Text(translate('Manage trusted devices')))
+ ],
+ ).marginOnly(left: 30);
+
return Column(
- children: [tfa, bot],
+ children: [tfa, bot, trust],
);
}
@@ -971,7 +1071,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate('Accept sessions via both'),
];
var modeInitialKey = model.approveMode;
- if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
+ if (!modeKeys.contains(modeInitialKey)) {
+ modeInitialKey = defaultOptionApproveMode;
+ }
final usePassword = model.approveMode != 'click';
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
@@ -1018,6 +1120,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open',
reverse: false, enabled: enabled),
+ if (bind.mainIsInstalled()) unlockPin()
]);
}
@@ -1085,7 +1188,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
- ).marginOnly(right: 15),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value &&
@@ -1242,7 +1345,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
- ).marginOnly(right: 15),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed:
@@ -1265,6 +1368,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
}(),
];
}
+
+ Widget unlockPin() {
+ bool enabled = !locked;
+ RxString unlockPin = bind.mainGetUnlockPin().obs;
+ update() async {
+ unlockPin.value = bind.mainGetUnlockPin();
+ }
+
+ onChanged(bool? checked) async {
+ changeUnlockPinDialog(unlockPin.value, update);
+ }
+
+ final isOptFixed = isOptionFixed(kOptionWhitelist);
+ return GestureDetector(
+ child: Obx(() => Row(
+ children: [
+ Checkbox(
+ value: unlockPin.isNotEmpty,
+ onChanged: enabled && !isOptFixed ? onChanged : null)
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(
+ translate('Unlock with PIN'),
+ style: TextStyle(color: disabledTextColor(context, enabled)),
+ ))
+ ],
+ )),
+ onTap: enabled
+ ? () {
+ onChanged(!unlockPin.isNotEmpty);
+ }
+ : null,
+ ).marginOnly(left: _kCheckBoxLeftMargin);
+ }
}
class _Network extends StatefulWidget {
@@ -1277,112 +1414,84 @@ class _Network extends StatefulWidget {
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
- bool locked = bind.mainIsInstalled();
+ bool locked = !isWeb && bind.mainIsInstalled();
+
+ final scrollController = ScrollController();
@override
Widget build(BuildContext context) {
super.build(context);
- bool enabled = !locked;
- final scrollController = ScrollController();
+ return ListView(controller: scrollController, children: [
+ _lock(locked, 'Unlock Network Settings', () {
+ locked = false;
+ setState(() => {});
+ }),
+ preventMouseKeyBuilder(
+ block: locked,
+ child: Column(children: [
+ network(context),
+ ]),
+ ),
+ ]).marginOnly(bottom: _kListViewBottomMargin);
+ }
+
+ Widget network(BuildContext context) {
final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
- bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ListView(
- controller: scrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- children: [
- _lock(locked, 'Unlock Network Settings', () {
- locked = false;
- setState(() => {});
- }),
- AbsorbPointer(
- absorbing: locked,
- child: Column(children: [
- if (!hideServer) server(enabled),
- if (!hideProxy)
- _Card(title: 'Proxy', children: [
- _Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
- enabled: enabled),
- ]),
- ]),
- ),
- ]).marginOnly(bottom: _kListViewBottomMargin));
- }
+ isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
- server(bool enabled) {
- // Simple temp wrapper for PR check
- tmpWrapper() {
- // Setting page is not modal, oldOptions should only be used when getting options, never when setting.
- Map oldOptions = jsonDecode(bind.mainGetOptionsSync());
- old(String key) {
- return (oldOptions[key] ?? '').trim();
- }
-
- RxString idErrMsg = ''.obs;
- RxString relayErrMsg = ''.obs;
- RxString apiErrMsg = ''.obs;
- var idController =
- TextEditingController(text: old('custom-rendezvous-server'));
- var relayController = TextEditingController(text: old('relay-server'));
- var apiController = TextEditingController(text: old('api-server'));
- var keyController = TextEditingController(text: old('key'));
- final controllers = [
- idController,
- relayController,
- apiController,
- keyController,
- ];
- final errMsgs = [
- idErrMsg,
- relayErrMsg,
- apiErrMsg,
- ];
-
- submit() async {
- bool result = await setServerConfig(
- null,
- errMsgs,
- ServerConfig(
- idServer: idController.text,
- relayServer: relayController.text,
- apiServer: apiController.text,
- key: keyController.text));
- if (result) {
- setState(() {});
- showToast(translate('Successful'));
- } else {
- showToast(translate('Failed'));
- }
- }
-
- bool secure = !enabled;
- return _Card(
- title: 'ID/Relay Server',
- title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
- children: [
- Column(
- children: [
- Obx(() => _LabeledTextField(context, 'ID Server', idController,
- idErrMsg.value, enabled, secure)),
- Obx(() => _LabeledTextField(context, 'Relay Server',
- relayController, relayErrMsg.value, enabled, secure)),
- Obx(() => _LabeledTextField(context, 'API Server',
- apiController, apiErrMsg.value, enabled, secure)),
- _LabeledTextField(
- context, 'Key', keyController, '', enabled, secure),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [_Button('Apply', submit, enabled: enabled)],
- ).marginOnly(top: 10),
- ],
- )
- ]);
+ if (hideServer && hideProxy) {
+ return Offstage();
}
- return tmpWrapper();
+ return _Card(
+ title: 'Network',
+ children: [
+ Container(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (!hideServer)
+ ListTile(
+ leading: Icon(Icons.dns_outlined, color: _accentColor),
+ title: Text(
+ translate('ID/Relay Server'),
+ style: TextStyle(fontSize: _kContentFontSize),
+ ),
+ enabled: !locked,
+ onTap: () => showServerSettings(gFFI.dialogManager),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ contentPadding: EdgeInsets.symmetric(horizontal: 16),
+ minLeadingWidth: 0,
+ horizontalTitleGap: 10,
+ ),
+ if (!hideServer && !hideProxy)
+ Divider(height: 1, indent: 16, endIndent: 16),
+ if (!hideProxy)
+ ListTile(
+ leading:
+ Icon(Icons.network_ping_outlined, color: _accentColor),
+ title: Text(
+ translate('Socks5/Http(s) Proxy'),
+ style: TextStyle(fontSize: _kContentFontSize),
+ ),
+ enabled: !locked,
+ onTap: changeSocks5Proxy,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ contentPadding: EdgeInsets.symmetric(horizontal: 16),
+ minLeadingWidth: 0,
+ horizontalTitleGap: 10,
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
}
}
@@ -1397,19 +1506,14 @@ class _DisplayState extends State<_Display> {
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ListView(
- controller: scrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- children: [
- viewStyle(context),
- scrollStyle(context),
- imageQuality(context),
- codec(context),
- privacyModeImpl(context),
- other(context),
- ]).marginOnly(bottom: _kListViewBottomMargin));
+ return ListView(controller: scrollController, children: [
+ viewStyle(context),
+ scrollStyle(context),
+ imageQuality(context),
+ codec(context),
+ if (!isWeb) privacyModeImpl(context),
+ other(context),
+ ]).marginOnly(bottom: _kListViewBottomMargin);
}
Widget viewStyle(BuildContext context) {
@@ -1632,15 +1736,12 @@ class _AccountState extends State<_Account> {
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ListView(
- physics: DraggableNeverScrollableScrollPhysics(),
- controller: scrollController,
- children: [
- _Card(title: 'Account', children: [accountAction(), useInfo()]),
- ],
- ).marginOnly(bottom: _kListViewBottomMargin));
+ return ListView(
+ controller: scrollController,
+ children: [
+ _Card(title: 'Account', children: [accountAction(), useInfo()]),
+ ],
+ ).marginOnly(bottom: _kListViewBottomMargin);
}
Widget accountAction() {
@@ -1737,18 +1838,14 @@ class _PluginState extends State<_Plugin> {
Widget build(BuildContext context) {
bind.pluginListReload();
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: ChangeNotifierProvider.value(
- value: pluginManager,
- child: Consumer(builder: (context, model, child) {
- return ListView(
- physics: DraggableNeverScrollableScrollPhysics(),
- controller: scrollController,
- children: model.plugins.map((entry) => pluginCard(entry)).toList(),
- ).marginOnly(bottom: _kListViewBottomMargin);
- }),
- ),
+ return ChangeNotifierProvider.value(
+ value: pluginManager,
+ child: Consumer(builder: (context, model, child) {
+ return ListView(
+ controller: scrollController,
+ children: model.plugins.map((entry) => pluginCard(entry)).toList(),
+ ).marginOnly(bottom: _kListViewBottomMargin);
+ }),
);
}
@@ -1800,74 +1897,72 @@ class _AboutState extends State<_About> {
final fingerprint = data['fingerprint'].toString();
const linkStyle = TextStyle(decoration: TextDecoration.underline);
final scrollController = ScrollController();
- return DesktopScrollWrapper(
- scrollController: scrollController,
- child: SingleChildScrollView(
- controller: scrollController,
- physics: DraggableNeverScrollableScrollPhysics(),
- child: _Card(title: translate('About RustDesk'), children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(
- height: 8.0,
- ),
- SelectionArea(
- child: Text('${translate('Version')}: $version')
- .marginSymmetric(vertical: 4.0)),
- SelectionArea(
- child: Text('${translate('Build Date')}: $buildDate')
- .marginSymmetric(vertical: 4.0)),
- SelectionArea(
- child: Text('${translate('Fingerprint')}: $fingerprint')
- .marginSymmetric(vertical: 4.0)),
- InkWell(
- onTap: () {
- launchUrlString('https://rustdesk.com/privacy.html');
- },
- child: Text(
- translate('Privacy Statement'),
- style: linkStyle,
- ).marginSymmetric(vertical: 4.0)),
- InkWell(
- onTap: () {
- launchUrlString('https://rustdesk.com');
- },
- child: Text(
- translate('Website'),
- style: linkStyle,
- ).marginSymmetric(vertical: 4.0)),
- Container(
- decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
- padding:
- const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
- child: SelectionArea(
- child: Row(
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
- style: const TextStyle(color: Colors.white),
- ),
- Text(
- translate('Slogan_tip'),
- style: TextStyle(
- fontWeight: FontWeight.w800,
- color: Colors.white),
- )
- ],
+ return SingleChildScrollView(
+ controller: scrollController,
+ child: _Card(title: translate('About RustDesk'), children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(
+ height: 8.0,
+ ),
+ SelectionArea(
+ child: Text('${translate('Version')}: $version')
+ .marginSymmetric(vertical: 4.0)),
+ SelectionArea(
+ child: Text('${translate('Build Date')}: $buildDate')
+ .marginSymmetric(vertical: 4.0)),
+ if (!isWeb)
+ SelectionArea(
+ child: Text('${translate('Fingerprint')}: $fingerprint')
+ .marginSymmetric(vertical: 4.0)),
+ InkWell(
+ onTap: () {
+ launchUrlString('https://rustdesk.com/privacy.html');
+ },
+ child: Text(
+ translate('Privacy Statement'),
+ style: linkStyle,
+ ).marginSymmetric(vertical: 4.0)),
+ InkWell(
+ onTap: () {
+ launchUrlString('https://rustdesk.com');
+ },
+ child: Text(
+ translate('Website'),
+ style: linkStyle,
+ ).marginSymmetric(vertical: 4.0)),
+ Container(
+ decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
+ padding:
+ const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
+ child: SelectionArea(
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
+ style: const TextStyle(color: Colors.white),
),
- ),
- ],
- )),
- ).marginSymmetric(vertical: 4.0)
- ],
- ).marginOnly(left: _kContentHMargin)
- ]),
- ));
+ Text(
+ translate('Slogan_tip'),
+ style: TextStyle(
+ fontWeight: FontWeight.w800,
+ color: Colors.white),
+ )
+ ],
+ ),
+ ),
+ ],
+ )),
+ ).marginSymmetric(vertical: 4.0)
+ ],
+ ).marginOnly(left: _kContentHMargin)
+ ]),
+ );
});
}
}
@@ -2160,9 +2255,14 @@ Widget _lock(
Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)),
onPressed: () async {
- bool checked = await callMainCheckSuperUserPermission();
- if (checked) {
- onUnlock();
+ final unlockPin = bind.mainGetUnlockPin();
+ if (unlockPin.isEmpty) {
+ bool checked = await callMainCheckSuperUserPermission();
+ if (checked) {
+ onUnlock();
+ }
+ } else {
+ checkUnlockPinDialog(unlockPin, onUnlock);
}
},
).marginSymmetric(horizontal: 2, vertical: 4),
@@ -2180,26 +2280,39 @@ _LabeledTextField(
String errorText,
bool enabled,
bool secure) {
- return Row(
+ return Table(
+ columnWidths: const {
+ 0: FixedColumnWidth(150),
+ 1: FlexColumnWidth(),
+ },
+ defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
- ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 140),
- child: Text(
- '${translate(label)}:',
- textAlign: TextAlign.right,
- style: TextStyle(
- fontSize: 16, color: disabledTextColor(context, enabled)),
- ).marginOnly(right: 10)),
- Expanded(
- child: TextField(
+ TableRow(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: Text(
+ '${translate(label)}:',
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ fontSize: 16,
+ color: disabledTextColor(context, enabled),
+ ),
+ ),
+ ),
+ TextField(
controller: controller,
enabled: enabled,
obscureText: secure,
+ autocorrect: false,
decoration: InputDecoration(
- errorText: errorText.isNotEmpty ? errorText : null),
+ errorText: errorText.isNotEmpty ? errorText : null,
+ ),
style: TextStyle(
color: disabledTextColor(context, enabled),
- )),
+ ),
+ ).workaroundFreezeLinuxMint(),
+ ],
),
],
).marginOnly(bottom: 8);
@@ -2377,7 +2490,7 @@ void changeSocks5Proxy() async {
controller: proxyController,
autofocus: true,
enabled: !isOptFixed,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2397,7 +2510,7 @@ void changeSocks5Proxy() async {
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed,
- ),
+ ).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2422,7 +2535,8 @@ void changeSocks5Proxy() async {
: Icons.visibility))),
controller: pwdController,
enabled: !isOptFixed,
- )),
+ maxLength: bind.mainMaxEncryptLen(),
+ ).workaroundFreezeLinuxMint()),
),
],
),
diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart
index 2e577e625ae0..6440e55a1117 100644
--- a/flutter/lib/desktop/pages/desktop_tab_page.dart
+++ b/flutter/lib/desktop/pages/desktop_tab_page.dart
@@ -37,13 +37,9 @@ class DesktopTabPage extends StatefulWidget {
}
}
-class _DesktopTabPageState extends State
- with WidgetsBindingObserver {
+class _DesktopTabPageState extends State {
final tabController = DesktopTabController(tabType: DesktopTabType.main);
- final RxBool _block = false.obs;
- // bool mouseIn = false;
-
_DesktopTabPageState() {
RemoteCountState.init();
Get.put(tabController);
@@ -69,19 +65,10 @@ class _DesktopTabPageState extends State
}
}
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {
- super.didChangeAppLifecycleState(state);
- if (state == AppLifecycleState.resumed) {
- shouldBeBlocked(_block, canBeBlocked);
- } else if (state == AppLifecycleState.inactive) {}
- }
-
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
- WidgetsBinding.instance.addObserver(this);
}
/*
@@ -97,7 +84,6 @@ class _DesktopTabPageState extends State
@override
void dispose() {
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
- WidgetsBinding.instance.removeObserver(this);
Get.delete();
super.dispose();
@@ -119,13 +105,13 @@ class _DesktopTabPageState extends State
isClose: false,
),
),
- blockTab: _block,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(
() => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: windowManagerEnableResizeEdges,
child: tabWidget,
),
);
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index 3b4428f99e4f..3f555dcaa66e 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
+import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
+import 'package:flutter_hbb/web/dummy.dart'
+ if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget {
required this.id,
required this.password,
required this.isSharedPassword,
- required this.tabController,
+ this.tabController,
+ this.connToken,
this.forceRelay})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
final bool? forceRelay;
- final DesktopTabController tabController;
+ final String? connToken;
+ final DesktopTabController? tabController;
@override
State createState() => _FileManagerPageState();
}
class _FileManagerPageState extends State
- with AutomaticKeepAliveClientMixin {
+ with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -87,6 +92,7 @@ class _FileManagerPageState extends State
isFileTransfer: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
+ connToken: widget.connToken,
forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
@@ -96,12 +102,16 @@ class _FileManagerPageState extends State
if (!isLinux) {
WakelockPlus.enable();
}
+ if (isWeb) {
+ _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
+ }
debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
- widget.tabController.onSelected?.call(widget.id);
+ widget.tabController?.onSelected?.call(widget.id);
});
+ WidgetsBinding.instance.addObserver(this);
}
@override
@@ -114,12 +124,21 @@ class _FileManagerPageState extends State
}
Get.delete(tag: 'ft_${widget.id}');
});
+ WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
bool get wantKeepAlive => true;
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ if (state == AppLifecycleState.resumed) {
+ jobController.jobTable.refresh();
+ }
+ }
+
@override
Widget build(BuildContext context) {
super.build(context);
@@ -129,10 +148,11 @@ class _FileManagerPageState extends State
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row(
children: [
- Flexible(
- flex: 3,
- child: dropArea(FileManagerView(
- model.localController, _ffi, _mouseFocusScope))),
+ if (!isWeb)
+ Flexible(
+ flex: 3,
+ child: dropArea(FileManagerView(
+ model.localController, _ffi, _mouseFocusScope))),
Flexible(
flex: 3,
child: dropArea(FileManagerView(
@@ -173,10 +193,31 @@ class _FileManagerPageState extends State
/// transfer status list
/// watch transfer status
Widget statusList() {
+ Widget getIcon(JobProgress job) {
+ final color = Theme.of(context).tabBarTheme.labelColor;
+ switch (job.type) {
+ case JobType.deleteDir:
+ case JobType.deleteFile:
+ return Icon(Icons.delete_outline, color: color);
+ default:
+ return Transform.rotate(
+ angle: isWeb
+ ? job.isRemoteToLocal
+ ? pi / 2
+ : pi / 2 * 3
+ : job.isRemoteToLocal
+ ? pi
+ : 0,
+ child: Icon(Icons.arrow_forward_ios, color: color),
+ );
+ }
+ }
+
statusListView(List jobs) => ListView.builder(
controller: ScrollController(),
itemBuilder: (BuildContext context, int index) {
final item = jobs[index];
+ final status = item.getStatus();
return Padding(
padding: const EdgeInsets.only(bottom: 5),
child: generateCard(
@@ -186,15 +227,8 @@ class _FileManagerPageState extends State
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Transform.rotate(
- angle: item.isRemoteToLocal ? pi : 0,
- child: SvgPicture.asset("assets/arrow.svg",
- colorFilter: svgColor(
- Theme.of(context).tabBarTheme.labelColor)),
- ).paddingOnly(left: 15),
- const SizedBox(
- width: 16.0,
- ),
+ getIcon(item)
+ .marginSymmetric(horizontal: 10, vertical: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -203,45 +237,28 @@ class _FileManagerPageState extends State
Tooltip(
waitDuration: Duration(milliseconds: 500),
message: item.jobName,
- child: Text(
- item.fileName,
+ child: ExtendedText(
+ item.jobName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
- ).paddingSymmetric(vertical: 10),
- ),
- Text(
- '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
- style: TextStyle(
- fontSize: 12,
- color: MyTheme.darkGray,
- ),
- ),
- Offstage(
- offstage: item.state != JobState.inProgress,
- child: Text(
- '${translate("Speed")} ${readableFileSize(item.speed)}/s',
- style: TextStyle(
- fontSize: 12,
- color: MyTheme.darkGray,
- ),
+ overflowWidget: TextOverflowWidget(
+ child: Text("..."),
+ position: TextOverflowPosition.start),
),
),
- Offstage(
- offstage: item.state == JobState.inProgress,
- child: Text(
- translate(
- item.display(),
- ),
- style: TextStyle(
- fontSize: 12,
- color: MyTheme.darkGray,
- ),
- ),
+ Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: status,
+ child: Text(status,
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ )).marginOnly(top: 6),
),
Offstage(
- offstage: item.state != JobState.inProgress,
+ offstage: item.type != JobType.transfer ||
+ item.state != JobState.inProgress,
child: LinearPercentIndicator(
- padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true,
center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -251,7 +268,7 @@ class _FileManagerPageState extends State
progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight,
- ).paddingSymmetric(vertical: 15),
+ ).paddingSymmetric(vertical: 8),
),
],
),
@@ -262,6 +279,7 @@ class _FileManagerPageState extends State
Offstage(
offstage: item.state != JobState.paused,
child: MenuButton(
+ tooltip: translate("Resume"),
onPressed: () {
jobController.resumeJob(item.id);
},
@@ -274,7 +292,7 @@ class _FileManagerPageState extends State
),
),
MenuButton(
- padding: EdgeInsets.only(right: 15),
+ tooltip: translate("Delete"),
child: SvgPicture.asset(
"assets/close.svg",
colorFilter: svgColor(Colors.white),
@@ -287,11 +305,11 @@ class _FileManagerPageState extends State
hoverColor: MyTheme.accent80,
),
],
- ),
+ ).marginAll(12),
],
),
],
- ).paddingSymmetric(vertical: 10),
+ ),
),
);
},
@@ -475,6 +493,9 @@ class _FileManagerViewState extends State {
}
Widget headTools() {
+ var uploadButtonTapPosition = RelativeRect.fill;
+ RxBool isUploadFolder =
+ (bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
return Container(
child: Column(
children: [
@@ -521,6 +542,7 @@ class _FileManagerViewState extends State {
Row(
children: [
MenuButton(
+ tooltip: translate('Back'),
padding: EdgeInsets.only(
right: 3,
),
@@ -540,6 +562,7 @@ class _FileManagerViewState extends State {
},
),
MenuButton(
+ tooltip: translate('Parent directory'),
child: RotatedBox(
quarterTurns: 3,
child: SvgPicture.asset(
@@ -604,6 +627,7 @@ class _FileManagerViewState extends State {
switch (_locationStatus.value) {
case LocationStatus.bread:
return MenuButton(
+ tooltip: translate('Search'),
onPressed: () {
_locationStatus.value = LocationStatus.fileSearchBar;
Future.delayed(
@@ -630,6 +654,7 @@ class _FileManagerViewState extends State {
);
case LocationStatus.fileSearchBar:
return MenuButton(
+ tooltip: translate('Clear'),
onPressed: () {
onSearchText("", isLocal);
_locationStatus.value = LocationStatus.bread;
@@ -645,6 +670,7 @@ class _FileManagerViewState extends State {
}
}),
MenuButton(
+ tooltip: translate('Refresh File'),
padding: EdgeInsets.only(
left: 3,
),
@@ -670,6 +696,7 @@ class _FileManagerViewState extends State {
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
MenuButton(
+ tooltip: translate('Home'),
padding: EdgeInsets.only(
right: 3,
),
@@ -685,11 +712,27 @@ class _FileManagerViewState extends State {
hoverColor: Theme.of(context).hoverColor,
),
MenuButton(
+ tooltip: translate('Create Folder'),
onPressed: () {
final name = TextEditingController();
+ String? errorText;
_ffi.dialogManager.show((setState, close, context) {
+ name.addListener(() {
+ if (errorText != null) {
+ setState(() {
+ errorText = null;
+ });
+ }
+ });
submit() {
if (name.value.text.isNotEmpty) {
+ if (!PathUtil.validName(name.value.text,
+ controller.options.value.isWindows)) {
+ setState(() {
+ errorText = translate("Invalid folder name");
+ });
+ return;
+ }
controller.createDir(PathUtil.join(
controller.directory.value.path,
name.value.text,
@@ -721,10 +764,11 @@ class _FileManagerViewState extends State {
labelText: translate(
"Please enter the folder name",
),
+ errorText: errorText,
),
controller: name,
autofocus: true,
- ),
+ ).workaroundFreezeLinuxMint(),
],
),
actions: [
@@ -754,6 +798,7 @@ class _FileManagerViewState extends State {
hoverColor: Theme.of(context).hoverColor,
),
Obx(() => MenuButton(
+ tooltip: translate('Delete'),
onPressed: SelectedItems.valid(selectedItems.items)
? () async {
await (controller
@@ -773,6 +818,66 @@ class _FileManagerViewState extends State {
],
),
),
+ if (isWeb)
+ Obx(() => ElevatedButton.icon(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(
+ isLocal
+ ? EdgeInsets.only(left: 10)
+ : EdgeInsets.only(right: 10)),
+ backgroundColor: MaterialStateProperty.all(
+ selectedItems.items.isEmpty
+ ? MyTheme.accent80
+ : MyTheme.accent,
+ ),
+ ),
+ onPressed: () =>
+ {webselectFiles(is_folder: isUploadFolder.value)},
+ label: InkWell(
+ hoverColor: Colors.transparent,
+ splashColor: Colors.transparent,
+ highlightColor: Colors.transparent,
+ focusColor: Colors.transparent,
+ onTapDown: (e) {
+ final x = e.globalPosition.dx;
+ final y = e.globalPosition.dy;
+ uploadButtonTapPosition =
+ RelativeRect.fromLTRB(x, y, x, y);
+ },
+ onTap: () async {
+ final value = await showMenu(
+ context: context,
+ position: uploadButtonTapPosition,
+ items: [
+ PopupMenuItem(
+ value: false,
+ child: Text(translate('Upload files')),
+ ),
+ PopupMenuItem(
+ value: true,
+ child: Text(translate('Upload folder')),
+ ),
+ ]);
+ if (value != null) {
+ isUploadFolder.value = value;
+ bind.mainSetLocalOption(
+ key: 'upload-folder-button',
+ value: value ? 'Y' : '');
+ webselectFiles(is_folder: value);
+ }
+ },
+ child: Icon(Icons.arrow_drop_down),
+ ),
+ icon: Text(
+ translate(isUploadFolder.isTrue
+ ? 'Upload folder'
+ : 'Upload files'),
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ color: Colors.white,
+ ),
+ ).marginOnly(left: 8),
+ )).marginOnly(left: 16),
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(
@@ -806,19 +911,22 @@ class _FileManagerViewState extends State {
: Colors.white,
),
)
- : RotatedBox(
- quarterTurns: 2,
- child: SvgPicture.asset(
- "assets/arrow.svg",
- colorFilter: svgColor(selectedItems.items.isEmpty
- ? Theme.of(context).brightness ==
- Brightness.light
- ? MyTheme.grayBg
- : MyTheme.darkGray
- : Colors.white),
- alignment: Alignment.bottomRight,
- ),
- ),
+ : isWeb
+ ? Offstage()
+ : RotatedBox(
+ quarterTurns: 2,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ colorFilter: svgColor(
+ selectedItems.items.isEmpty
+ ? Theme.of(context).brightness ==
+ Brightness.light
+ ? MyTheme.grayBg
+ : MyTheme.darkGray
+ : Colors.white),
+ alignment: Alignment.bottomRight,
+ ),
+ ),
label: isLocal
? SvgPicture.asset(
"assets/arrow.svg",
@@ -830,7 +938,7 @@ class _FileManagerViewState extends State {
: Colors.white),
)
: Text(
- translate('Receive'),
+ translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle(
color: selectedItems.items.isEmpty
? Theme.of(context).brightness ==
@@ -885,6 +993,7 @@ class _FileManagerViewState extends State {
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
child: MenuButton(
+ tooltip: translate('More'),
onPressed: () => mod_menu.showMenu(
context: context,
position: menuPos,
@@ -916,6 +1025,7 @@ class _FileManagerViewState extends State {
BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value;
final entries = fd.entries;
+ Rx rightClickEntry = Rx(null);
return ListSearchActionListener(
node: _keyboardNode,
@@ -974,16 +1084,70 @@ class _FileManagerViewState extends State {
final lastModifiedStr = entry.isDrive
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
+ var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
+ onTap() {
+ final items = selectedItems;
+ // handle double click
+ if (_checkDoubleClick(entry)) {
+ controller.openDirectory(entry.path);
+ items.clear();
+ return;
+ }
+ _onSelectedChanged(items, filteredEntries, entry, isLocal);
+ }
+
+ onSecondaryTap() {
+ final items = [
+ if (!entry.isDrive &&
+ versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
+ mod_menu.PopupMenuItem(
+ child: Text(translate("Rename")),
+ height: CustomPopupMenuTheme.height,
+ onTap: () {
+ controller.renameAction(entry, isLocal);
+ },
+ )
+ ];
+ if (items.isNotEmpty) {
+ rightClickEntry.value = entry;
+ final future = mod_menu.showMenu(
+ context: context,
+ position: secondaryPosition,
+ items: items,
+ );
+ future.then((value) {
+ rightClickEntry.value = null;
+ });
+ future.onError((error, stackTrace) {
+ rightClickEntry.value = null;
+ });
+ }
+ }
+
+ onSecondaryTapDown(details) {
+ secondaryPosition = RelativeRect.fromLTRB(
+ details.globalPosition.dx,
+ details.globalPosition.dy,
+ details.globalPosition.dx,
+ details.globalPosition.dy);
+ }
+
return Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container(
decoration: BoxDecoration(
color: selectedItems.items.contains(entry)
- ? Theme.of(context).hoverColor
+ ? MyTheme.button
: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(5.0),
),
+ border: rightClickEntry.value == entry
+ ? Border.all(
+ color: MyTheme.button,
+ width: 1.0,
+ )
+ : null,
),
key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight,
@@ -1022,22 +1186,19 @@ class _FileManagerViewState extends State {
),
Expanded(
child: Text(entry.name.nonBreaking,
+ style: TextStyle(
+ color: selectedItems.items
+ .contains(entry)
+ ? Colors.white
+ : null),
overflow:
TextOverflow.ellipsis))
]),
)),
),
- onTap: () {
- final items = selectedItems;
- // handle double click
- if (_checkDoubleClick(entry)) {
- controller.openDirectory(entry.path);
- items.clear();
- return;
- }
- _onSelectedChanged(
- items, filteredEntries, entry, isLocal);
- },
+ onTap: onTap,
+ onSecondaryTap: onSecondaryTap,
+ onSecondaryTapDown: onSecondaryTapDown,
),
SizedBox(
width: 2.0,
@@ -1054,11 +1215,17 @@ class _FileManagerViewState extends State {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
- color: MyTheme.darkGray,
+ color: selectedItems.items
+ .contains(entry)
+ ? Colors.white70
+ : MyTheme.darkGray,
),
)),
),
),
+ onTap: onTap,
+ onSecondaryTap: onSecondaryTap,
+ onSecondaryTapDown: onSecondaryTapDown,
),
// Divider from header.
SizedBox(
@@ -1074,9 +1241,16 @@ class _FileManagerViewState extends State {
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(
- fontSize: 10, color: MyTheme.darkGray),
+ fontSize: 10,
+ color:
+ selectedItems.items.contains(entry)
+ ? Colors.white70
+ : MyTheme.darkGray),
),
),
+ onTap: onTap,
+ onSecondaryTap: onSecondaryTap,
+ onSecondaryTapDown: onSecondaryTapDown,
),
),
],
@@ -1483,7 +1657,7 @@ class _FileManagerViewState extends State {
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal)
: null,
- ),
+ ).workaroundFreezeLinuxMint(),
)
],
);
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index a68e4feecdc3..cc77cdd95819 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State {
isSharedPassword: params['isSharedPassword'],
tabController: tabController,
forceRelay: params['forceRelay'],
+ connToken: params['connToken'],
)));
}
@@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State {
super.initState();
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
- print(
+ debugPrint(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
// for simplify, just replace connectionId
if (call.method == kWindowEventNewFileTransfer) {
@@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State {
isSharedPassword: args['isSharedPassword'],
tabController: tabController,
forceRelay: args['forceRelay'],
+ connToken: args['connToken'],
)));
} else if (call.method == "onDestroy") {
tabController.clear();
@@ -111,6 +113,7 @@ class _FileManagerTabPageState extends State {
: SubWindowDragToResizeArea(
child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
);
}
diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart
index a860fe89ea1c..756367c21f1f 100644
--- a/flutter/lib/desktop/pages/install_page.dart
+++ b/flutter/lib/desktop/pages/install_page.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
@@ -41,6 +43,7 @@ class _InstallPageState extends State {
Widget build(BuildContext context) {
return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: windowManagerEnableResizeEdges,
child: Container(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
@@ -73,6 +76,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
_InstallPageBodyState() {
controller = TextEditingController(text: bind.installInstallPath());
+ final installOptions = jsonDecode(bind.installInstallOptions());
+ startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
+ desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
}
@override
@@ -141,7 +147,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
),
- ).marginOnly(right: 10),
+ ).workaroundFreezeLinuxMint().marginOnly(right: 10),
),
Obx(
() => OutlinedButton.icon(
@@ -249,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (desktopicon.value) args += ' desktopicon';
bind.installInstallMe(options: args, path: controller.text);
}
+
do_install();
}
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index 5541cb8b33b9..6671d041bbf5 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
required this.isRDP,
required this.isSharedPassword,
this.forceRelay,
+ this.connToken,
}) : super(key: key);
final String id;
final String? password;
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
final bool isRDP;
final bool? forceRelay;
final bool? isSharedPassword;
+ final String? connToken;
@override
State createState() => _PortForwardPageState();
@@ -62,6 +64,7 @@ class _PortForwardPageState extends State
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
+ connToken: widget.connToken,
isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}");
@@ -235,7 +238,7 @@ class _PortForwardPageState extends State
inputFormatters: inputFormatters,
decoration: InputDecoration(
hintText: hint,
- ))),
+ )).workaroundFreezeLinuxMint()),
);
}
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index 5534db85549b..f399f7cab687 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State {
tabController: tabController,
isRDP: isRDP,
forceRelay: params['forceRelay'],
+ connToken: params['connToken'],
)));
}
@@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State {
isRDP: isRDP,
tabController: tabController,
forceRelay: args['forceRelay'],
+ connToken: args['connToken'],
)));
} else if (call.method == "onDestroy") {
tabController.clear();
@@ -127,6 +129,7 @@ class _PortForwardTabPageState extends State {
() => SubWindowDragToResizeArea(
child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
),
);
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index 341025c5f3c9..912b06b0282b 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
-import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart';
@@ -115,6 +114,8 @@ class _RemotePageState extends State
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
+ _ffi.recordingModel
+ .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.start(
widget.id,
@@ -245,13 +246,14 @@ class _RemotePageState extends State
super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onRemotePageDispose(closeSession);
- // ensure we leave this session, this is a double check
- _ffi.inputModel.enterOrLeave(false);
+ if (closeSession) {
+ // ensure we leave this session, this is a double check
+ _ffi.inputModel.enterOrLeave(false);
+ }
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
- _ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
@@ -739,12 +741,6 @@ class _ImagePaintState extends State {
ScrollController horizontal,
ScrollController vertical,
) {
- final scrollConfig = CustomMouseWheelScrollConfig(
- scrollDuration: kDefaultScrollDuration,
- scrollCurve: Curves.linearToEaseOut,
- mouseWheelTurnsThrottleTimeMs:
- kDefaultMouseWheelThrottleDuration.inMilliseconds,
- scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
var widget = child;
if (layoutSize.width < size.width) {
widget = ScrollConfiguration(
@@ -790,36 +786,26 @@ class _ImagePaintState extends State {
);
}
if (layoutSize.width < size.width) {
- widget = ImprovedScrolling(
- scrollController: horizontal,
- enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
- customMouseWheelScrollConfig: scrollConfig,
- child: RawScrollbar(
- thickness: kScrollbarThickness,
- thumbColor: Colors.grey,
- controller: horizontal,
- thumbVisibility: false,
- trackVisibility: false,
- notificationPredicate: layoutSize.height < size.height
- ? (notification) => notification.depth == 1
- : defaultScrollNotificationPredicate,
- child: widget,
- ),
+ widget = RawScrollbar(
+ thickness: kScrollbarThickness,
+ thumbColor: Colors.grey,
+ controller: horizontal,
+ thumbVisibility: false,
+ trackVisibility: false,
+ notificationPredicate: layoutSize.height < size.height
+ ? (notification) => notification.depth == 1
+ : defaultScrollNotificationPredicate,
+ child: widget,
);
}
if (layoutSize.height < size.height) {
- widget = ImprovedScrolling(
- scrollController: vertical,
- enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
- customMouseWheelScrollConfig: scrollConfig,
- child: RawScrollbar(
- thickness: kScrollbarThickness,
- thumbColor: Colors.grey,
- controller: vertical,
- thumbVisibility: false,
- trackVisibility: false,
- child: widget,
- ),
+ widget = RawScrollbar(
+ thickness: kScrollbarThickness,
+ thumbColor: Colors.grey,
+ controller: vertical,
+ thumbVisibility: false,
+ trackVisibility: false,
+ child: widget,
);
}
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 94535bc6dcfc..efd437e1ff74 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -228,6 +228,7 @@ class _ConnectionTabPageState extends State {
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
));
}
@@ -394,7 +395,7 @@ class _ConnectionTabPageState extends State {
RemoteCountState.find().value = tabController.length;
Future _remoteMethodHandler(call, fromWindowId) async {
- print(
+ debugPrint(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index a052437479b7..95d9f2c7c7d5 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -83,7 +83,7 @@ class _DesktopServerPageState extends State
child: Consumer(
builder: (context, serverModel, child) {
final body = Scaffold(
- backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: ConnectionManager(),
);
return isLinux
@@ -110,7 +110,8 @@ class ConnectionManager extends StatefulWidget {
class ConnectionManagerState extends State
with WidgetsBindingObserver {
- final RxBool _block = false.obs;
+ final RxBool _controlPageBlock = false.obs;
+ final RxBool _sidePageBlock = false.obs;
ConnectionManagerState() {
gFFI.serverModel.tabController.onSelected = (client_id_str) {
@@ -139,7 +140,8 @@ class ConnectionManagerState extends State
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) {
- shouldBeBlocked(_block, null);
+ shouldBeBlocked(_controlPageBlock, null);
+ shouldBeBlocked(_sidePageBlock, null);
}
}
}
@@ -192,9 +194,6 @@ class ConnectionManagerState extends State
selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100,
tail: null, //buildScrollJumper(),
- blockTab: allowRemoteCMModification() ? null : _block,
- selectedTabBackgroundColor:
- Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key);
@@ -229,7 +228,7 @@ class ConnectionManagerState extends State
borderWidth;
final realChatPageWidth =
constrains.maxWidth - realClosedWidth;
- return Row(children: [
+ final row = Row(children: [
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width)
Consumer(
@@ -239,14 +238,25 @@ class ConnectionManagerState extends State
? buildSidePage()
: buildRemoteBlock(
child: buildSidePage(),
- block: _block,
+ block: _sidePageBlock,
mask: true),
)),
SizedBox(
width: realClosedWidth,
- child:
- SizedBox(width: realClosedWidth, child: pageView)),
+ child: SizedBox(
+ width: realClosedWidth,
+ child: allowRemoteCMModification()
+ ? pageView
+ : buildRemoteBlock(
+ child: _buildKeyEventBlock(pageView),
+ block: _controlPageBlock,
+ mask: false,
+ ))),
]);
+ return Container(
+ color: Theme.of(context).scaffoldBackgroundColor,
+ child: row,
+ );
},
),
),
@@ -266,6 +276,10 @@ class ConnectionManagerState extends State
}
}
+ Widget _buildKeyEventBlock(Widget child) {
+ return ExcludeFocus(child: child, excluding: true);
+ }
+
Widget buildTitleBar() {
return SizedBox(
height: kDesktopRemoteTabBarHeight,
@@ -1155,6 +1169,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
Text(translate('Create Folder'))
],
);
+ case CmFileAction.rename:
+ return Column(
+ children: [
+ Icon(
+ Icons.drive_file_move_outlined,
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ Text(translate('Rename'))
+ ],
+ );
}
}
diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
index 7828dd4a0ec6..0984eea5801c 100644
--- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
localPlatform = kPeerPlatformWindows;
} else if (isLinux) {
localPlatform = kPeerPlatformLinux;
+ } else if (isWebOnWindows || isWebOnLinux) {
+ localPlatform = kPeerPlatformWebDesktop;
}
- // to-do: web desktop support ?
return localPlatform;
}
diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart
index 17b160fed0ac..8fc90de114b9 100644
--- a/flutter/lib/desktop/widgets/menu_button.dart
+++ b/flutter/lib/desktop/widgets/menu_button.dart
@@ -34,6 +34,7 @@ class _MenuButtonState extends State {
return Padding(
padding: widget.padding,
child: Tooltip(
+ waitDuration: Duration(milliseconds: 300),
message: widget.tooltip,
child: Material(
type: MaterialType.transparency,
diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart
index 98fa676144b4..d826ea8c6b60 100644
--- a/flutter/lib/desktop/widgets/remote_toolbar.dart
+++ b/flutter/lib/desktop/widgets/remote_toolbar.dart
@@ -305,7 +305,7 @@ class RemoteMenuEntry {
}) {
return MenuEntryButton(
childBuilder: (TextStyle? style) => Text(
- '${translate("Insert")} Ctrl + Alt + Del',
+ translate("Insert Ctrl + Alt + Del"),
style: style,
),
proc: () {
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State {
shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius,
child: _DraggableShowHide(
+ id: widget.id,
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fractionX: _fractionX,
@@ -452,8 +453,8 @@ class _RemoteToolbarState extends State {
Widget _buildToolbar(BuildContext context) {
final List toolbarItems = [];
+ toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) {
- toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
}
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State {
setFullscreen: _setFullscreen,
));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
+ toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) {
- toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
}
if (!isWeb) toolbarItems.add(_RecordMenu());
@@ -1494,7 +1495,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
);
}
- TextField _resolutionInput(TextEditingController controller) {
+ Widget _resolutionInput(TextEditingController controller) {
return TextField(
decoration: InputDecoration(
border: InputBorder.none,
@@ -1508,7 +1509,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
controller: controller,
- );
+ ).workaroundFreezeLinuxMint();
}
List _supportedResolutionMenuButtons() => resolutions
@@ -1612,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
// If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported.
String? modeOnly;
- if (isInputSourceFlutter) {
+ // Keep both map and legacy mode on web at the moment.
+ // TODO: Remove legacy mode after web supports translate mode on web.
+ if (isInputSourceFlutter && isDesktop) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode;
@@ -1716,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
- ffiModel.setViewOnly(id, value);
+ final viewOnly = await bind.sessionGetToggleOption(
+ sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
+ ffiModel.setViewOnly(id, viewOnly ?? value);
}
: null,
ffi: ffi,
@@ -1776,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
@override
Widget build(BuildContext context) {
- return _IconSubmenuButton(
- tooltip: 'Chat',
- key: chatButtonKey,
- svg: 'assets/chat.svg',
- ffi: widget.ffi,
- color: _ToolbarTheme.blueColor,
- hoverColor: _ToolbarTheme.hoverBlueColor,
- menuChildrenGetter: () => [textChat(), voiceCall()]);
+ if (isWeb) {
+ return buildTextChatButton();
+ } else {
+ return _IconSubmenuButton(
+ tooltip: 'Chat',
+ key: chatButtonKey,
+ svg: 'assets/chat.svg',
+ ffi: widget.ffi,
+ color: _ToolbarTheme.blueColor,
+ hoverColor: _ToolbarTheme.hoverBlueColor,
+ menuChildrenGetter: () => [textChat(), voiceCall()]);
+ }
+ }
+
+ buildTextChatButton() {
+ return _IconMenuButton(
+ assetName: 'assets/message_24dp_5F6368.svg',
+ tooltip: 'Text chat',
+ key: chatButtonKey,
+ onPressed: _textChatOnPressed,
+ color: _ToolbarTheme.blueColor,
+ hoverColor: _ToolbarTheme.hoverBlueColor,
+ );
}
textChat() {
return MenuButton(
child: Text(translate('Text chat')),
ffi: widget.ffi,
- onPressed: () {
- RenderBox? renderBox =
- chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
-
- Offset? initPos;
- if (renderBox != null) {
- final pos = renderBox.localToGlobal(Offset.zero);
- initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
- }
+ onPressed: _textChatOnPressed);
+ }
- widget.ffi.chatModel.changeCurrentKey(
- MessageKey(widget.ffi.id, ChatModel.clientModeID));
- widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
- });
+ _textChatOnPressed() {
+ RenderBox? renderBox =
+ chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
+ Offset? initPos;
+ if (renderBox != null) {
+ final pos = renderBox.localToGlobal(Offset.zero);
+ initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
+ }
+ widget.ffi.chatModel
+ .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
+ widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
}
voiceCall() {
@@ -1904,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
var ffi = Provider.of(context);
var recordingModel = Provider.of(context);
final visible =
- (recordingModel.start || ffi.permissions['recording'] != false) &&
- ffi.pi.currentDisplay != kAllDisplayValue;
+ (recordingModel.start || ffi.permissions['recording'] != false);
if (!visible) return Offstage();
return _IconMenuButton(
assetName: 'assets/rec.svg',
@@ -2214,6 +2233,7 @@ class RdoMenuButton extends StatelessWidget {
}
class _DraggableShowHide extends StatefulWidget {
+ final String id;
final SessionID sessionId;
final RxDouble fractionX;
final RxBool dragging;
@@ -2225,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
const _DraggableShowHide({
Key? key,
+ required this.id,
required this.sessionId,
required this.fractionX,
required this.dragging,
@@ -2314,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
final isFullscreen = stateGlobal.fullscreen;
const double iconSize = 20;
+
+ buttonWrapper(VoidCallback? onPressed, Widget child,
+ {Color hoverColor = _ToolbarTheme.blueColor}) {
+ final bgColor = buttonStyle.backgroundColor?.resolve({});
+ return TextButton(
+ onPressed: onPressed,
+ child: child,
+ style: buttonStyle.copyWith(
+ backgroundColor: MaterialStateProperty.resolveWith((states) {
+ if (states.contains(MaterialState.hovered)) {
+ return (bgColor ?? hoverColor).withOpacity(0.15);
+ }
+ return bgColor;
+ }),
+ ),
+ );
+ }
+
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
- Obx(() => TextButton(
- onPressed: () {
+ Obx(() => buttonWrapper(
+ () {
widget.setFullscreen(!isFullscreen.value);
},
- child: Tooltip(
+ Tooltip(
message: translate(
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon(
@@ -2333,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
),
),
)),
- if (!isMacOS)
+ if (!isMacOS && !isWebDesktop)
Obx(() => Offstage(
offstage: isFullscreen.isFalse,
- child: TextButton(
- onPressed: () => widget.setMinimize(),
- child: Tooltip(
+ child: buttonWrapper(
+ widget.setMinimize,
+ Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
@@ -2347,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
),
),
)),
- TextButton(
- onPressed: () => setState(() {
+ buttonWrapper(
+ () => setState(() {
widget.toolbarState.switchShow(widget.sessionId);
}),
- child: Obx((() => Tooltip(
+ Obx((() => Tooltip(
message:
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
@@ -2360,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
),
))),
),
+ if (isWebDesktop)
+ Obx(() {
+ if (show.isTrue) {
+ return Offstage();
+ } else {
+ return buttonWrapper(
+ () => closeConnection(id: widget.id),
+ Tooltip(
+ message: translate('Close'),
+ child: Icon(
+ Icons.close,
+ size: iconSize,
+ color: _ToolbarTheme.redColor,
+ ),
+ ),
+ hoverColor: _ToolbarTheme.redColor,
+ ).paddingOnly(left: iconSize / 2);
+ }
+ })
],
);
return TextButtonTheme(
diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart
deleted file mode 100644
index c5bc3394b439..000000000000
--- a/flutter/lib/desktop/widgets/scroll_wrapper.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-import 'package:flutter/widgets.dart';
-import 'package:flutter_hbb/consts.dart';
-import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
-
-class DesktopScrollWrapper extends StatelessWidget {
- final ScrollController scrollController;
- final Widget child;
- const DesktopScrollWrapper(
- {Key? key, required this.scrollController, required this.child})
- : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return ImprovedScrolling(
- scrollController: scrollController,
- enableCustomMouseWheelScrolling: true,
- // enableKeyboardScrolling: true, // strange behavior on mac
- customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
- scrollDuration: kDefaultScrollDuration,
- scrollCurve: Curves.linearToEaseOut,
- mouseWheelTurnsThrottleTimeMs:
- kDefaultMouseWheelThrottleDuration.inMilliseconds,
- scrollAmountMultiplier: kDefaultScrollAmountMultiplier),
- child: child,
- );
- }
-}
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index fca6efb2e9ba..96ada22c9072 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -246,7 +246,6 @@ class DesktopTab extends StatefulWidget {
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor;
- final RxBool? blockTab;
final DesktopTabController controller;
@@ -272,7 +271,6 @@ class DesktopTab extends StatefulWidget {
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
this.selectedBorderColor,
- this.blockTab,
}) : super(key: key);
static RxString tablabelGetter(String peerId) {
@@ -311,7 +309,6 @@ class _DesktopTabState extends State
Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor;
- RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller;
RxList