diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml
new file mode 100644
index 000000000..90a3d4fb3
--- /dev/null
+++ b/.github/workflows/winget.yml
@@ -0,0 +1,15 @@
+name: Publish to WinGet
+on:
+ release:
+ types: [released]
+ workflow_dispatch:
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: vedantmgoyal9/winget-releaser@main
+ with:
+ identifier: RustDesk.RustDesk
+ version: "1.4.6"
+ release-tag: "1.4.6"
+ token: ${{ secrets.WINGET_TOKEN }}
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index e36c65fab..000000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# RustDesk Guide
-
-## Project Layout
-
-### Directory Structure
-* `src/` Rust app
-* `src/server/` audio / clipboard / input / video / network
-* `src/platform/` platform-specific code
-* `src/ui/` legacy Sciter UI (deprecated)
-* `flutter/` current UI
-* `libs/hbb_common/` config / proto / shared utils
-* `libs/scrap/` screen capture
-* `libs/enigo/` input control
-* `libs/clipboard/` clipboard
-* `libs/hbb_common/src/config.rs` all options
-
-### Key Components
-- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
-- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
-- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
-- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
-- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
-
-### UI Architecture
-- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
-- **Modern UI**: Flutter-based - files in `flutter/`
- - Desktop: `flutter/lib/desktop/`
- - Mobile: `flutter/lib/mobile/`
- - Shared: `flutter/lib/common/` and `flutter/lib/models/`
-
-## Rust Rules
-
-* Avoid `unwrap()` / `expect()` in production code.
-* Exceptions:
-
- * tests;
- * lock acquisition where failure means poisoning, not normal control flow.
-* Otherwise prefer `Result` + `?` or explicit handling.
-* Do not ignore errors silently.
-* Avoid unnecessary `.clone()`.
-* Prefer borrowing when practical.
-* Do not add dependencies unless needed.
-* Keep code simple and idiomatic.
-
-## Tokio Rules
-
-* Assume a Tokio runtime already exists.
-* Never create nested runtimes.
-* Never call `Runtime::block_on()` inside Tokio / async code.
-* Do not hide runtime creation inside helpers or libraries.
-* Do not hold locks across `.await`.
-* Prefer `.await`, `tokio::spawn`, channels.
-* Use `spawn_blocking` or dedicated threads for blocking work.
-* Do not use `std::thread::sleep()` in async code.
-
-## Editing Hygiene
-
-* Change only what is required.
-* Prefer the smallest valid diff.
-* Do not refactor unrelated code.
-* Do not make formatting-only changes.
-* Keep naming/style consistent with nearby code.
diff --git a/CLAUDE.md b/CLAUDE.md
index c31706425..8d46e1fa1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1 +1,91 @@
-AGENTS.md
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Build Commands
+- `cargo run` - Build and run the desktop application (requires libsciter library)
+- `python3 build.py --flutter` - Build Flutter version (desktop)
+- `python3 build.py --flutter --release` - Build Flutter version in release mode
+- `python3 build.py --hwcodec` - Build with hardware codec support
+- `python3 build.py --vram` - Build with VRAM feature (Windows only)
+- `cargo build --release` - Build Rust binary in release mode
+- `cargo build --features hwcodec` - Build with specific features
+
+### Flutter Mobile Commands
+- `cd flutter && flutter build android` - Build Android APK
+- `cd flutter && flutter build ios` - Build iOS app
+- `cd flutter && flutter run` - Run Flutter app in development mode
+- `cd flutter && flutter test` - Run Flutter tests
+
+### Testing
+- `cargo test` - Run Rust tests
+- `cd flutter && flutter test` - Run Flutter tests
+
+### Platform-Specific Build Scripts
+- `flutter/build_android.sh` - Android build script
+- `flutter/build_ios.sh` - iOS build script
+- `flutter/build_fdroid.sh` - F-Droid build script
+
+## Project Architecture
+
+### Directory Structure
+- **`src/`** - Main Rust application code
+ - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
+ - `src/server/` - Audio/clipboard/input/video services and network connections
+ - `src/client.rs` - Peer connection handling
+ - `src/platform/` - Platform-specific code
+- **`flutter/`** - Flutter UI code for desktop and mobile
+- **`libs/`** - Core libraries
+ - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
+ - `libs/scrap/` - Screen capture functionality
+ - `libs/enigo/` - Platform-specific keyboard/mouse control
+ - `libs/clipboard/` - Cross-platform clipboard implementation
+
+### Key Components
+- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
+- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
+- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
+- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
+- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
+
+### UI Architecture
+- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
+- **Modern UI**: Flutter-based - files in `flutter/`
+ - Desktop: `flutter/lib/desktop/`
+ - Mobile: `flutter/lib/mobile/`
+ - Shared: `flutter/lib/common/` and `flutter/lib/models/`
+
+## Important Build Notes
+
+### Dependencies
+- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
+- Set `VCPKG_ROOT` environment variable
+- Download appropriate Sciter library for legacy UI support
+
+### Ignore Patterns
+When working with files, ignore these directories:
+- `target/` - Rust build artifacts
+- `flutter/build/` - Flutter build output
+- `flutter/.dart_tool/` - Flutter tooling files
+
+### Cross-Platform Considerations
+- Windows builds require additional DLLs and virtual display drivers
+- macOS builds need proper signing and notarization for distribution
+- Linux builds support multiple package formats (deb, rpm, AppImage)
+- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
+
+### Feature Flags
+- `hwcodec` - Hardware video encoding/decoding
+- `vram` - VRAM optimization (Windows only)
+- `flutter` - Enable Flutter UI
+- `unix-file-copy-paste` - Unix file clipboard support
+- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
+
+### Config
+All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
+- Settings
+- Local
+- Display
+- Built-in
diff --git a/Cargo.lock b/Cargo.lock
index fe1f67cc0..06cfeeb96 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,12 +33,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
-[[package]]
-name = "adler2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-
[[package]]
name = "aead"
version = "0.5.2"
@@ -299,8 +293,8 @@ dependencies = [
"image 0.25.1",
"log",
"objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2-app-kit",
+ "objc2-foundation",
"parking_lot",
"percent-encoding",
"serde 1.0.228",
@@ -643,7 +637,7 @@ dependencies = [
"cc",
"cfg-if 1.0.0",
"libc",
- "miniz_oxide 0.7.4",
+ "miniz_oxide",
"object",
"rustc-demangle",
]
@@ -866,15 +860,6 @@ dependencies = [
"objc2 0.5.2",
]
-[[package]]
-name = "block2"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
-dependencies = [
- "objc2 0.6.4",
-]
-
[[package]]
name = "blocking"
version = "1.6.1"
@@ -1197,7 +1182,7 @@ dependencies = [
"js-sys",
"num-traits 0.2.19",
"wasm-bindgen",
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -1305,8 +1290,8 @@ dependencies = [
"lazy_static",
"libc",
"objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2-app-kit",
+ "objc2-foundation",
"once_cell",
"parking_lot",
"percent-encoding",
@@ -2231,15 +2216,6 @@ dependencies = [
"dirs-sys 0.4.1",
]
-[[package]]
-name = "dirs"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
-dependencies = [
- "dirs-sys 0.5.0",
-]
-
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -2257,7 +2233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
- "redox_users 0.4.5",
+ "redox_users",
"winapi 0.3.9",
]
@@ -2269,22 +2245,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
- "redox_users 0.4.5",
+ "redox_users",
"windows-sys 0.48.0",
]
-[[package]]
-name = "dirs-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users 0.5.2",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -2292,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
- "redox_users 0.4.5",
+ "redox_users",
"winapi 0.3.9",
]
@@ -2302,16 +2266,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
-[[package]]
-name = "dispatch2"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
-dependencies = [
- "bitflags 2.9.1",
- "objc2 0.6.4",
-]
-
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -2761,7 +2715,7 @@ dependencies = [
"flume",
"half",
"lebe",
- "miniz_oxide 0.7.4",
+ "miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
@@ -2847,12 +2801,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
-version = "1.1.9"
+version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
- "miniz_oxide 0.8.9",
+ "miniz_oxide",
]
[[package]]
@@ -4087,7 +4041,7 @@ dependencies = [
"gif",
"jpeg-decoder",
"num-traits 0.2.19",
- "png 0.17.13",
+ "png",
"qoi",
"tiff",
]
@@ -4101,7 +4055,7 @@ dependencies = [
"bytemuck",
"byteorder",
"num-traits 0.2.19",
- "png 0.17.13",
+ "png",
"tiff",
]
@@ -4812,16 +4766,6 @@ dependencies = [
"simd-adler32",
]
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
- "simd-adler32",
-]
-
[[package]]
name = "mio"
version = "0.8.11"
@@ -4872,23 +4816,21 @@ dependencies = [
[[package]]
name = "muda"
-version = "0.17.1"
+version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
+checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
dependencies = [
+ "cocoa 0.25.0",
"crossbeam-channel",
"dpi",
"gtk",
"keyboard-types",
"libxdo",
- "objc2 0.6.4",
- "objc2-app-kit 0.3.2",
- "objc2-core-foundation",
- "objc2-foundation 0.3.2",
+ "objc",
"once_cell",
- "png 0.17.13",
- "thiserror 2.0.17",
- "windows-sys 0.60.2",
+ "png",
+ "thiserror 1.0.61",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -5432,16 +5374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
dependencies = [
"objc-sys 0.3.5",
- "objc2-encode 4.1.0",
-]
-
-[[package]]
-name = "objc2"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
-dependencies = [
- "objc2-encode 4.1.0",
+ "objc2-encode 4.0.3",
]
[[package]]
@@ -5456,22 +5389,10 @@ dependencies = [
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
"objc2-quartz-core",
]
-[[package]]
-name = "objc2-app-kit"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
-dependencies = [
- "bitflags 2.9.1",
- "objc2 0.6.4",
- "objc2-core-foundation",
- "objc2-foundation 0.3.2",
-]
-
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
@@ -5482,7 +5403,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5493,7 +5414,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5505,28 +5426,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
-]
-
-[[package]]
-name = "objc2-core-foundation"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
-dependencies = [
- "bitflags 2.9.1",
- "dispatch2",
- "objc2 0.6.4",
-]
-
-[[package]]
-name = "objc2-core-graphics"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
-dependencies = [
- "bitflags 2.9.1",
- "objc2-core-foundation",
+ "objc2-foundation",
]
[[package]]
@@ -5537,7 +5437,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
"objc2-metal",
]
@@ -5550,7 +5450,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-contacts",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5564,9 +5464,9 @@ dependencies = [
[[package]]
name = "objc2-encode"
-version = "4.1.0"
+version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
[[package]]
name = "objc2-foundation"
@@ -5581,18 +5481,6 @@ dependencies = [
"objc2 0.5.2",
]
-[[package]]
-name = "objc2-foundation"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
-dependencies = [
- "bitflags 2.9.1",
- "block2 0.6.2",
- "objc2 0.6.4",
- "objc2-core-foundation",
-]
-
[[package]]
name = "objc2-link-presentation"
version = "0.2.2"
@@ -5601,8 +5489,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2-app-kit",
+ "objc2-foundation",
]
[[package]]
@@ -5614,7 +5502,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5626,7 +5514,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
"objc2-metal",
]
@@ -5637,7 +5525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5653,7 +5541,7 @@ dependencies = [
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
@@ -5669,7 +5557,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5682,7 +5570,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
- "objc2-foundation 0.2.2",
+ "objc2-foundation",
]
[[package]]
@@ -5996,8 +5884,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
-version = "0.7.3-6"
-source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
+version = "0.7.3-5"
+source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
dependencies = [
"futures",
"libc",
@@ -6290,20 +6178,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
- "miniz_oxide 0.7.4",
-]
-
-[[package]]
-name = "png"
-version = "0.18.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
-dependencies = [
- "bitflags 2.9.1",
- "crc32fast",
- "fdeflate",
- "flate2",
- "miniz_oxide 0.8.9",
+ "miniz_oxide",
]
[[package]]
@@ -6988,17 +6863,6 @@ dependencies = [
"thiserror 1.0.61",
]
-[[package]]
-name = "redox_users"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
-dependencies = [
- "getrandom 0.2.15",
- "libredox",
- "thiserror 2.0.17",
-]
-
[[package]]
name = "regex"
version = "1.11.1"
@@ -8117,8 +7981,8 @@ dependencies = [
"log",
"memmap2",
"objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2-app-kit",
+ "objc2-foundation",
"objc2-quartz-core",
"raw-window-handle 0.6.2",
"redox_syscall 0.5.2",
@@ -8448,7 +8312,7 @@ dependencies = [
"objc",
"once_cell",
"parking_lot",
- "png 0.17.13",
+ "png",
"raw-window-handle 0.6.2",
"scopeguard",
"tao-macros",
@@ -8702,7 +8566,7 @@ dependencies = [
"bytemuck",
"cfg-if 1.0.0",
"log",
- "png 0.17.13",
+ "png",
"tiny-skia-path",
]
@@ -9075,22 +8939,21 @@ dependencies = [
[[package]]
name = "tray-icon"
-version = "0.21.3"
-source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6"
+version = "0.14.3"
+source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f"
dependencies = [
+ "core-graphics 0.23.2",
"crossbeam-channel",
- "dirs 6.0.0",
+ "dirs 5.0.1",
"libappindicator",
"muda",
- "objc2 0.6.4",
- "objc2-app-kit 0.3.2",
- "objc2-core-foundation",
- "objc2-core-graphics",
- "objc2-foundation 0.3.2",
+ "objc2 0.5.2",
+ "objc2-app-kit",
+ "objc2-foundation",
"once_cell",
- "png 0.18.1",
- "thiserror 2.0.17",
- "windows-sys 0.60.2",
+ "png",
+ "thiserror 1.0.61",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -10195,7 +10058,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.0",
"windows-future",
- "windows-link 0.1.1",
+ "windows-link",
"windows-numerics",
]
@@ -10244,7 +10107,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
- "windows-link 0.1.1",
+ "windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
@@ -10256,7 +10119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -10309,12 +10172,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -10322,7 +10179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -10340,7 +10197,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -10360,7 +10217,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -10369,7 +10226,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
- "windows-link 0.1.1",
+ "windows-link",
]
[[package]]
@@ -10399,24 +10256,6 @@ dependencies = [
"windows-targets 0.52.6",
]
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link 0.2.1",
-]
-
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -10456,30 +10295,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
+ "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
-[[package]]
-name = "windows-targets"
-version = "0.53.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
-dependencies = [
- "windows-link 0.2.1",
- "windows_aarch64_gnullvm 0.53.1",
- "windows_aarch64_msvc 0.53.1",
- "windows_i686_gnu 0.53.1",
- "windows_i686_gnullvm 0.53.1",
- "windows_i686_msvc 0.53.1",
- "windows_x86_64_gnu 0.53.1",
- "windows_x86_64_gnullvm 0.53.1",
- "windows_x86_64_msvc 0.53.1",
-]
-
[[package]]
name = "windows-version"
version = "0.1.1"
@@ -10516,12 +10338,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -10552,12 +10368,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -10588,24 +10398,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-[[package]]
-name = "windows_i686_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
-
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -10636,12 +10434,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -10672,12 +10464,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -10696,12 +10482,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -10732,12 +10512,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-
[[package]]
name = "winit"
version = "0.30.9"
@@ -10762,8 +10536,8 @@ dependencies = [
"memmap2",
"ndk 0.9.0",
"objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2-app-kit",
+ "objc2-foundation",
"objc2-ui-kit",
"orbclient",
"percent-encoding",
diff --git a/Cargo.toml b/Cargo.toml
index fa22dcd7b..d792d5cd5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -160,7 +160,7 @@ piet-coregraphics = "0.6"
foreign-types = "0.3"
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
-tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" }
+tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
image = "0.24"
@@ -245,6 +245,3 @@ panic = 'abort'
strip = true
#opt-level = 'z' # only have smaller size after strip
rpath = true
-
-[profile.dev]
-debug = 1
diff --git a/GEMINI.md b/GEMINI.md
deleted file mode 100644
index c31706425..000000000
--- a/GEMINI.md
+++ /dev/null
@@ -1 +0,0 @@
-AGENTS.md
diff --git a/build.py b/build.py
index 5c53e4fc8..ce9a09ef6 100755
--- a/build.py
+++ b/build.py
@@ -512,7 +512,7 @@ def main():
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
- system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
+ system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
elif os.path.isfile('/usr/bin/pacman'):
# pacman -S -needed base-devel
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
diff --git a/docs/CODE_OF_CONDUCT-FR.md b/docs/CODE_OF_CONDUCT-FR.md
deleted file mode 100644
index dca61e0aa..000000000
--- a/docs/CODE_OF_CONDUCT-FR.md
+++ /dev/null
@@ -1,143 +0,0 @@
-
-# Code de conduite des contributeurs
-
-## Notre engagement
-
-En tant que membres, contributeurs et responsables, nous nous engageons à faire
-de la participation à notre communauté une expérience exempte de harcèlement pour
-tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou
-invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité
-et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut
-socio-économique, de la nationalité, de l'apparence personnelle, de la race, de
-la religion ou de l'identité et de l'orientation sexuelle.
-
-Nous nous engageons à agir et à interagir de manière à contribuer à une
-communauté ouverte, accueillante, diversifiée, inclusive et saine.
-
-## Nos standards
-
-Exemples de comportements qui contribuent à un environnement positif pour notre
-communauté :
-
-* Faire preuve d'empathie et de bienveillance envers les autres
-* Respecter les opinions, les points de vue et les expériences différents
-* Donner et accepter gracieusement les retours constructifs
-* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos
- erreurs et apprendre de l'expérience
-* Se concentrer sur ce qui est le mieux non seulement pour nous en tant
- qu'individus, mais pour l'ensemble de la communauté
-
-Exemples de comportements inacceptables :
-
-* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou
- avances sexuelles de quelque nature que ce soit
-* Le trolling, les commentaires insultants ou désobligeants, et les attaques
- personnelles ou politiques
-* Le harcèlement public ou privé
-* La publication d'informations privées d'autrui, telles qu'une adresse physique
- ou électronique, sans autorisation explicite
-* Tout autre comportement qui pourrait raisonnablement être considéré comme
- inapproprié dans un cadre professionnel
-
-## Responsabilités en matière d'application
-
-Les responsables de la communauté sont chargés de clarifier et d'appliquer nos
-standards de comportement acceptable et prendront des mesures correctives
-appropriées et équitables en réponse à tout comportement qu'ils jugent
-inapproprié, menaçant, offensant ou nuisible.
-
-Les responsables de la communauté ont le droit et la responsabilité de
-supprimer, modifier ou rejeter les commentaires, commits, code, modifications
-du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de
-conduite, et communiqueront les raisons de leurs décisions de modération le cas
-échéant.
-
-## Portée
-
-Ce Code de conduite s'applique dans tous les espaces communautaires, et
-s'applique également lorsqu'une personne représente officiellement la communauté
-dans les espaces publics. Les exemples de représentation de notre communauté
-incluent l'utilisation d'une adresse e-mail officielle, la publication via un
-compte de réseau social officiel, ou le fait d'agir en tant que représentant
-désigné lors d'un événement en ligne ou hors ligne.
-
-## Application
-
-Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent
-être signalés aux responsables de la communauté chargés de l'application à
-[info@rustdesk.com](mailto:info@rustdesk.com).
-Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et
-équitable.
-
-Tous les responsables de la communauté sont tenus de respecter la vie privée et
-la sécurité de la personne ayant signalé un incident.
-
-## Directives d'application
-
-Les responsables de la communauté suivront ces Directives d'impact communautaire
-pour déterminer les conséquences de toute action qu'ils jugent en violation de ce
-Code de conduite :
-
-### 1. Correction
-
-**Impact communautaire** : Utilisation d'un langage inapproprié ou autre
-comportement jugé non professionnel ou indésirable dans la communauté.
-
-**Conséquence** : Un avertissement écrit et privé de la part des responsables de
-la communauté, expliquant la nature de la violation et pourquoi le comportement
-était inapproprié. Des excuses publiques peuvent être demandées.
-
-### 2. Avertissement
-
-**Impact communautaire** : Une violation par un incident isolé ou une série
-d'actions.
-
-**Conséquence** : Un avertissement avec des conséquences en cas de comportement
-répété. Aucune interaction avec les personnes impliquées, y compris les
-interactions non sollicitées avec les personnes chargées d'appliquer le Code de
-conduite, pendant une période déterminée. Cela inclut d'éviter les interactions
-dans les espaces communautaires ainsi que dans les canaux externes comme les
-réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion
-temporaire ou permanente.
-
-### 3. Exclusion temporaire
-
-**Impact communautaire** : Une violation grave des standards communautaires, y
-compris un comportement inapproprié persistant.
-
-**Conséquence** : Une exclusion temporaire de toute interaction ou communication
-publique avec la communauté pendant une période déterminée. Aucune interaction
-publique ou privée avec les personnes impliquées, y compris les interactions non
-sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est
-autorisée pendant cette période. Le non-respect de ces conditions peut entraîner
-une exclusion permanente.
-
-### 4. Exclusion permanente
-
-**Impact communautaire** : Démontrer un schéma de violation des standards
-communautaires, y compris un comportement inapproprié persistant, le harcèlement
-d'une personne, ou une agression envers des catégories de personnes ou leur
-dénigrement.
-
-**Conséquence** : Une exclusion permanente de toute interaction publique au sein
-de la communauté.
-
-## Attribution
-
-Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0,
-disponible à l'adresse
-[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
-
-Les Directives d'impact communautaire ont été inspirées par
-[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC].
-
-Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la
-FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions
-sont disponibles à l'adresse
-[https://www.contributor-covenant.org/translations][translations].
-
-[homepage]: https://www.contributor-covenant.org
-[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
-[Mozilla CoC]: https://github.com/mozilla/diversity
-[FAQ]: https://www.contributor-covenant.org/faq
-[translations]: https://www.contributor-covenant.org/translations
diff --git a/docs/CONTRIBUTING-FR.md b/docs/CONTRIBUTING-FR.md
deleted file mode 100644
index 6f800de7d..000000000
--- a/docs/CONTRIBUTING-FR.md
+++ /dev/null
@@ -1,55 +0,0 @@
-
-# Contribuer à RustDesk
-
-RustDesk accueille les contributions de tous. Voici les directives si vous
-envisagez de nous aider :
-
-## Contributions
-
-Les contributions à RustDesk ou à ses dépendances doivent être soumises sous
-forme de pull requests GitHub. Chaque pull request sera examinée par un
-contributeur principal (une personne ayant la permission d'intégrer des
-correctifs) et sera soit intégrée dans la branche principale, soit accompagnée
-de retours sur les modifications requises. Toutes les contributions doivent
-suivre ce format, même celles des contributeurs principaux.
-
-Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en
-commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela
-permet d'éviter les efforts en double de la part des contributeurs sur la même
-issue.
-
-## Liste de vérification pour les pull requests
-
-- Partez de la branche master et, si nécessaire, effectuez un rebase sur la
- branche master actuelle avant de soumettre votre pull request. Si elle ne
- fusionne pas proprement avec master, il vous sera peut-être demandé de
- rebaser vos modifications.
-
-- Les commits doivent être aussi petits que possible, tout en s'assurant que
- chaque commit est correct de manière indépendante (c.-à-d. que chaque commit
- doit compiler et passer les tests).
-
-- Les commits doivent être accompagnés d'une signature Developer Certificate of
- Origin (http://developercertificate.org), indiquant que vous (et votre
- employeur le cas échéant) acceptez d'être liés par les termes de la
- [licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de
- `git commit`.
-
-- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne
- spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une
- revue dans la pull request ou un commentaire, ou vous pouvez demander une
- revue par [e-mail](mailto:info@rustdesk.com).
-
-- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité.
-
-Pour des instructions git spécifiques, consultez le
-[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
-
-## Conduite
-
-https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
-
-## Communication
-
-Les contributeurs de RustDesk se retrouvent fréquemment sur
-[Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/README-FR.md b/docs/README-FR.md
index 345e53b58..c2e25886d 100644
--- a/docs/README-FR.md
+++ b/docs/README-FR.md
@@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- - Linux/macOS : vcpkg install libvpx libyuv opus aom
+ - Linux/Osx : vcpkg install libvpx libyuv opus aom
-- Exécutez `cargo run`
+- Exécuter `cargo run`
## Comment compiler/build sous Linux
@@ -93,7 +93,7 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+Exécution du cargo
```
## Comment construire avec Docker
diff --git a/docs/SECURITY-FR.md b/docs/SECURITY-FR.md
deleted file mode 100644
index 1cf2c6167..000000000
--- a/docs/SECURITY-FR.md
+++ /dev/null
@@ -1,16 +0,0 @@
-
-# Politique de sécurité
-
-## Signaler une vulnérabilité
-
-Nous accordons une très grande importance à la sécurité du projet. Nous
-encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils
-découvrent.
-
-Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez
-la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com.
-
-À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite
-équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler
-toute vulnérabilité de manière responsable afin que nous puissions continuer à
-développer une application sécurisée pour l'ensemble de la communauté.
diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml
index 90bdafcb5..0d3b33bb8 100644
--- a/flatpak/com.rustdesk.RustDesk.metainfo.xml
+++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml
@@ -18,7 +18,7 @@
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 privilege locally or from remote on demand.
+ 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.
@@ -56,4 +56,4 @@
pointing
-
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt
index 05742d7fd..db222dc84 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt
@@ -62,13 +62,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
return false
}
}
- val recorder = try {
- builder.build()
- } catch (e: Exception) {
- Log.e(logTag, "createAudioRecorder failed", e)
- return false
- }
- audioRecorder = recorder
+ audioRecorder = builder.build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return true
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt
index 6dd4a2f61..696d536c6 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt
@@ -311,10 +311,7 @@ class FloatingWindowService : Service(), View.OnTouchListener {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
}
val idStopService = 2
- val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y"
- if (!hideStopService) {
- popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
- }
+ popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
idShowRustDesk -> {
@@ -392,3 +389,4 @@ class FloatingWindowService : Service(), View.OnTouchListener {
return false
}
}
+
diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt
index e3c9d9830..8e9b39968 100644
--- a/flutter/android/app/src/main/kotlin/ffi.kt
+++ b/flutter/android/app/src/main/kotlin/ffi.kt
@@ -24,7 +24,6 @@ object FFI {
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
- external fun getBuildinOption(key: String): String
external fun onClipboardUpdate(clips: ByteBuffer)
external fun isServiceClipboardEnabled(): Boolean
}
diff --git a/flutter/assets/auth-microsoft.svg b/flutter/assets/auth-microsoft.svg
deleted file mode 100644
index c9ce5f9cf..000000000
--- a/flutter/assets/auth-microsoft.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh
index d50a6a6ce..ecfb444ef 100755
--- a/flutter/build_fdroid.sh
+++ b/flutter/build_fdroid.sh
@@ -7,7 +7,7 @@
# 2024, Vasyl Gello
#
-# The script is invoked by F-Droid builder system step-by-step.
+# The script is invoked by F-Droid builder system ste-by-step.
#
# It accepts the following arguments:
#
@@ -16,6 +16,7 @@
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
# - The build step to execute:
#
+# + sudo-deps: as root, install needed Debian packages into builder VM
# + prebuild: patch sources and do other stuff before the build
# + build: perform actual build of APK file
#
@@ -183,9 +184,13 @@ prebuild)
fi
# Map NDK version to revision
- NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
- jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
- sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
+
+ NDK_VERSION="$(wget \
+ -qO- \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ 'https://api.github.com/repos/android/ndk/releases' |
+ jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
@@ -311,18 +316,6 @@ prebuild)
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
- # Find first libclang.so and set BRIDGE_LLVM_PATH
-
- BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)"
-
- if [ -z "${BRIDGE_LLVM_PATH}" ]; then
- echo 'ERROR: Can not find libclang.so for bridge generator!' >&2
- exit 1
- fi
-
- BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
- BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
-
# Install Flutter bridge version
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
@@ -351,8 +344,7 @@ prebuild)
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
- --dart-output ./flutter/lib/generated_bridge.dart \
- --llvm-path "${BRIDGE_LLVM_PATH}"
+ --dart-output ./flutter/lib/generated_bridge.dart
# Add bridge files to save-list
@@ -363,15 +355,13 @@ prebuild)
git checkout '*'
git clean -dffx
git reset
-
- unset BRIDGE_LLVM_PATH
fi
# Install Flutter version for RustDesk library build
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
- # gms is not in these files now, but we still keep the following line for future reference(maybe).
+ # gms is not in thoes files now, but we still keep the following line for future reference(maybe).
sed \
-i \
@@ -424,9 +414,13 @@ build)
.github/workflows/flutter-build.yml)"
# Map NDK version to revision
- NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
- jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
- sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
+
+ NDK_VERSION="$(wget \
+ -qO- \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ 'https://api.github.com/repos/android/ndk/releases' |
+ jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 366a7b6ba..af87f980f 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -716,17 +716,6 @@ closeConnection({String? id}) {
stateGlobal.isInMainPage = true;
} else {
final controller = Get.find();
- if (controller.tabType == DesktopTabType.terminal &&
- controller.onCloseWindow != null) {
- // Terminal windows are scoped to one peer. The optional id passed to
- // closeConnection() is that peer id, not a terminal tab key
- // (${peerId}_${terminalId}). Closing from terminal dialogs should close
- // the peer's whole terminal window, including all terminal tabs.
- unawaited(controller.onCloseWindow!().catchError((e, _) {
- debugPrint('[closeConnection] Failed to close terminal window: $e');
- }));
- return;
- }
controller.closeBy(id);
}
}
@@ -2376,19 +2365,6 @@ List? urlLinkToCmdArgs(Uri uri) {
id = uri.path.substring("/new/".length);
} else if (uri.authority == "config") {
if (isAndroid || isIOS) {
- final allowDeepLinkServerSettings =
- bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) ==
- 'Y';
- if (!allowDeepLinkServerSettings) {
- debugPrint(
- "Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled.");
- // Keep the user-facing error generic; detailed rejection reason is in debug logs.
- // Delay toast to avoid missing overlay during cold-start deeplink handling.
- Timer(Duration(seconds: 1), () {
- showToast(translate('Failed'));
- });
- return null;
- }
final config = uri.path.substring("/".length);
// add a timer to make showToast work
Timer(Duration(seconds: 1), () {
@@ -2398,24 +2374,11 @@ List? urlLinkToCmdArgs(Uri uri) {
return null;
} else if (uri.authority == "password") {
if (isAndroid || isIOS) {
- final allowDeepLinkPassword =
- bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y';
- if (!allowDeepLinkPassword) {
- debugPrint(
- "Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled.");
- // Keep the user-facing error generic; detailed rejection reason is in debug logs.
- // Delay toast to avoid missing overlay during cold-start deeplink handling.
- Timer(Duration(seconds: 1), () {
- showToast(translate('Failed'));
- });
- return null;
- }
final password = uri.path.substring("/".length);
if (password.isNotEmpty) {
Timer(Duration(seconds: 1), () async {
- final ok =
- await bind.mainSetPermanentPasswordWithResult(password: password);
- showToast(translate(ok ? 'Successful' : 'Failed'));
+ await bind.mainSetPermanentPassword(password: password);
+ showToast(translate('Successful'));
});
}
}
@@ -4190,7 +4153,8 @@ Widget? buildAvatarWidget({
width: size,
height: size,
fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(),
+ errorBuilder: (_, __, ___) =>
+ fallback ?? SizedBox.shrink(),
),
);
}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 054a1666c..1a09d6f53 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -54,9 +54,9 @@ class _AddressBookState extends State {
const LinearProgressIndicator(),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
- err: gFFI.abModel.abPullError,
+ err: gFFI.abModel.currentAbPullError,
retry: null,
- close: gFFI.abModel.clearPullErrors),
+ close: () => gFFI.abModel.currentAbPullError.value = ''),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPushError,
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index ee376de68..62ade8e51 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -20,8 +20,7 @@ const kOpSvgList = [
'okta',
'facebook',
'azure',
- 'auth0',
- 'microsoft'
+ 'auth0'
];
class _IconOP extends StatelessWidget {
@@ -225,59 +224,21 @@ class _WidgetOPState extends State {
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- if (_stateMsg.isNotEmpty && _failedMsg.isEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: SelectableText(
- translate(_stateMsg),
- style: DefaultTextStyle.of(context)
- .style
- .copyWith(fontSize: 12),
- ),
- ),
- if (_failedMsg.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: Builder(builder: (context) {
- final errorColor =
- Theme.of(context).colorScheme.error;
- final bgColor = Theme.of(context)
- .colorScheme
- .errorContainer
- .withOpacity(0.3);
- return Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8.0, vertical: 6.0),
- decoration: BoxDecoration(
- color: bgColor,
- borderRadius: BorderRadius.circular(4.0),
+ child: RichText(
+ text: TextSpan(
+ text: '$_stateMsg ',
+ style:
+ DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
+ children: [
+ TextSpan(
+ text: _failedMsg,
+ style: DefaultTextStyle.of(context).style.copyWith(
+ fontSize: 14,
+ color: Colors.red,
),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.error_outline,
- color: errorColor, size: 16),
- const SizedBox(width: 6),
- Flexible(
- child: SelectableText(
- translate(_failedMsg),
- style: DefaultTextStyle.of(context)
- .style
- .copyWith(
- fontSize: 13,
- color: errorColor,
- ),
- ),
- ),
- ],
- ),
- );
- }),
),
- ],
+ ],
+ ),
),
);
}),
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index 9515ca759..e35da6424 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget {
// 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 separated key events for en-US input method.
+ // while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope(
autofocus: true,
child: Focus(
@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => TapGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ), (instance) {
+ () => TapGestureRecognizer(), (instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => DoubleTapGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ), (instance) {
+ () => DoubleTapGestureRecognizer(), (instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap;
}),
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => LongPressGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ), (instance) {
+ () => LongPressGestureRecognizer(), (instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => HoldTapMoveGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ),
+ () => HoldTapMoveGestureRecognizer(),
(instance) => instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
..onHoldDragEnd = onHoldDragEnd),
DoubleFinerTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => DoubleFinerTapGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ), (instance) {
+ () => DoubleFinerTapGestureRecognizer(), (instance) {
instance
..onDoubleFinerTap = onDoubleFinerTap
..onDoubleFinerTapDown = onDoubleFinerTapDown;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
- () => CustomTouchGestureRecognizer(
- supportedDevices: kTouchBasedDeviceKinds,
- ), (instance) {
+ () => CustomTouchGestureRecognizer(), (instance) {
instance.onOneFingerPanStart =
(DragStartDetails d) => onOneFingerPanStart(context, d);
instance
diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart
index 537014246..a46ce54fd 100644
--- a/flutter/lib/common/widgets/toolbar.dart
+++ b/flutter/lib/common/widgets/toolbar.dart
@@ -16,12 +16,6 @@ import 'package:get/get.dart';
bool isEditOsPassword = false;
-// macOS privacy mode blacks out all online displays, so switching the remote
-// display does not weaken the local privacy protection.
-bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
- return pi.platform == kPeerPlatformMacOS;
-}
-
class TTextMenu {
final Widget child;
final VoidCallback? onPressed;
@@ -281,6 +275,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
isDesktop &&
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
+ pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu(
@@ -690,9 +685,8 @@ Future> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
- final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
- (privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
+ PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -766,25 +760,15 @@ List toolbarPrivacyMode(
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final sessionId = ffi.sessionId;
- final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false;
-
- // Backend revocation already attempts to turn privacy mode off.
- // Still keep this menu when privacy mode is active, so users can turn it off
- // if there is a sync delay, version mismatch, or off attempt failure.
- if (!hasPrivacyModePermission && privacyModeState.isEmpty) {
- return []; // No permission and not active, hide options.
- }
getDefaultMenu(Future Function(SessionID sid, String opt) toggleFunc) {
- final enabled =
- !ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
+ final enabled = !ffi.ffiModel.viewOnly;
return TToggleMenu(
value: privacyModeState.isNotEmpty,
onChanged: enabled
? (value) {
if (value == null) return;
- if (!allowDisplaySwitchInPrivacyMode(pi) &&
- ffiModel.pi.currentDisplay != 0 &&
+ if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
@@ -827,29 +811,18 @@ List toolbarPrivacyMode(
})
];
} else {
- final visibleImpls = hasPrivacyModePermission
- ? privacyModeImpls
- : privacyModeImpls.where((e) {
- final implKey = (e as List)[0] as String;
- return privacyModeState.value == implKey;
- }).toList();
- return visibleImpls.map((e) {
+ return privacyModeImpls.map((e) {
final implKey = (e as List)[0] as String;
final implName = (e)[1] as String;
- final enabled = !ffiModel.viewOnly &&
- (hasPrivacyModePermission || privacyModeState.value == implKey);
return TToggleMenu(
child: Text(translate(implName)),
value: privacyModeState.value == implKey,
- onChanged: enabled
- ? (value) {
- if (value == null) return;
- if (value && !hasPrivacyModePermission) return;
- togglePrivacyModeTime = DateTime.now();
- bind.sessionTogglePrivacyMode(
- sessionId: sessionId, implKey: implKey, on: value);
- }
- : null);
+ onChanged: (value) {
+ if (value == null) return;
+ togglePrivacyModeTime = DateTime.now();
+ bind.sessionTogglePrivacyMode(
+ sessionId: sessionId, implKey: implKey, on: value);
+ });
}).toList();
}
}
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index adf7b1d45..3b9940c9c 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -114,9 +114,6 @@ const String kOptionTerminalPersistent = "terminal-persistent";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
-const String kOptionEnablePrivacyMode = "enable-privacy-mode";
-const String kOptionEnablePermChangeInAcceptWindow =
- "enable-perm-change-in-accept-window";
const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method";
@@ -142,10 +139,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
-const String kOptionRemoteMenubarEdge = "remote-menubar-edge";
-const String kOptionRemoteMenubarFraction = "remote-menubar-frac";
-const String kOptionAllowMultiEdgeToolbarDock =
- "allow-multi-edge-toolbar-dock";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";
@@ -182,7 +175,6 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
-const String kOptionHideStopService = "hide-stop-service";
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
@@ -194,9 +186,6 @@ const String kOptionDisableChangeId = "disable-change-id";
const String kOptionDisableUnlockPin = "disable-unlock-pin";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
-const String kOptionAllowDeepLinkPassword = "allow-deep-link-password";
-const String kOptionAllowDeepLinkServerSettings =
- "allow-deep-link-server-settings";
const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 42ec10032..339ecddb0 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -908,17 +908,12 @@ class _DesktopHomePageState extends State
}
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
- final p0 = TextEditingController(text: "");
- final p1 = TextEditingController(text: "");
+ final pw = await bind.mainGetPermanentPassword();
+ final p0 = TextEditingController(text: pw);
+ final p1 = TextEditingController(text: pw);
var errMsg0 = "";
var errMsg1 = "";
- final localPasswordSet =
- (await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
- final permanentPasswordSet =
- (await bind.mainGetCommon(key: "permanent-password-set")) == "true";
- final presetPassword = permanentPasswordSet && !localPasswordSet;
- var canSubmit = false;
- final RxString rxPass = "".obs;
+ final RxString rxPass = pw.trim().obs;
final rules = [
DigitValidationRule(),
UppercaseValidationRule(),
@@ -927,21 +922,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
MinCharactersValidationRule(8),
];
final maxLength = bind.mainMaxEncryptLen();
- final statusTip = localPasswordSet
- ? translate('password-hidden-tip')
- : (presetPassword ? translate('preset-password-in-use-tip') : '');
- final showStatusTipOnMobile =
- statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
gFFI.dialogManager.show((setState, close, context) {
- updateCanSubmit() {
- canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
- }
-
- submit() async {
- if (!canSubmit) {
- return;
- }
+ submit() {
setState(() {
errMsg0 = "";
errMsg1 = "";
@@ -964,13 +947,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
return;
}
- final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
- if (!ok) {
- setState(() {
- errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
- });
- return;
- }
+ bind.mainSetPermanentPassword(password: pass);
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
@@ -978,20 +955,14 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
}
return CustomAlertDialog(
- title: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.key, color: MyTheme.accent),
- Text(translate("Set Password")).paddingOnly(left: 10),
- ],
- ),
+ title: Text(translate("Set Password")),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- SizedBox(
- height: showStatusTipOnMobile ? 0.0 : 6.0,
+ const SizedBox(
+ height: 8.0,
),
Row(
children: [
@@ -1007,7 +978,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
rxPass.value = value.trim();
setState(() {
errMsg0 = '';
- updateCanSubmit();
});
},
maxLength: maxLength,
@@ -1019,9 +989,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
children: [
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
],
- ).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
- SizedBox(
- height: showStatusTipOnMobile ? 0.0 : 8.0,
+ ).marginSymmetric(vertical: 8),
+ const SizedBox(
+ height: 8.0,
),
Row(
children: [
@@ -1035,7 +1005,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
onChanged: (value) {
setState(() {
errMsg1 = '';
- updateCanSubmit();
});
},
maxLength: maxLength,
@@ -1043,23 +1012,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
),
],
),
- if (statusTip.isNotEmpty)
- Row(
- children: [
- Icon(Icons.info, color: Colors.amber, size: 18)
- .marginOnly(right: 6),
- Expanded(
- child: Text(
- statusTip,
- style: const TextStyle(fontSize: 13, height: 1.1),
- ))
- ],
- ).marginOnly(top: 6, bottom: 2),
- SizedBox(
- height: showStatusTipOnMobile ? 0.0 : 8.0,
+ const SizedBox(
+ height: 8.0,
),
Obx(() => Wrap(
- runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
+ runSpacing: 8,
spacing: 4,
children: rules.map((e) {
var checked = e.validate(rxPass.value.trim());
@@ -1079,67 +1036,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
],
),
),
- actions: (() {
- final cancelButton = dialogButton(
- "Cancel",
- icon: Icon(Icons.close_rounded),
- onPressed: close,
- isOutline: true,
- );
- final removeButton = dialogButton(
- "Remove",
- icon: Icon(Icons.delete_outline_rounded),
- onPressed: () async {
- setState(() {
- errMsg0 = "";
- errMsg1 = "";
- });
- final ok =
- await bind.mainSetPermanentPasswordWithResult(password: "");
- if (!ok) {
- setState(() {
- errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
- });
- return;
- }
- close();
- },
- buttonStyle: ButtonStyle(
- backgroundColor: MaterialStatePropertyAll(Colors.red)),
- );
- final okButton = dialogButton(
- "OK",
- icon: Icon(Icons.done_rounded),
- onPressed: canSubmit ? submit : null,
- );
- if (!isDesktop && !isWebDesktop && localPasswordSet) {
- return [
- Align(
- alignment: Alignment.centerRight,
- child: FittedBox(
- fit: BoxFit.scaleDown,
- alignment: Alignment.centerRight,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- cancelButton,
- const SizedBox(width: 4),
- removeButton,
- const SizedBox(width: 4),
- okButton,
- ],
- ),
- ),
- ),
- ];
- }
- return [
- cancelButton,
- if (localPasswordSet) removeButton,
- okButton,
- ];
- })(),
- onSubmit: canSubmit ? submit : null,
+ actions: [
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
+ ],
+ onSubmit: submit,
onCancel: close,
);
});
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index d1d620014..82212d191 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -458,27 +458,18 @@ class _GeneralState extends State<_General> {
return const Offstage();
}
- final hideStopService =
- bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
-
- return Obx(() {
- if (hideStopService && !serviceStop.value) {
- return const Offstage();
- }
-
- return _Card(title: 'Service', children: [
- _Button(serviceStop.value ? 'Start' : 'Stop', () {
- () async {
- serviceBtnEnabled.value = false;
- await start_service(serviceStop.value);
- // enable the button after 1 second
- Future.delayed(const Duration(seconds: 1), () {
- serviceBtnEnabled.value = true;
- });
- }();
- }, enabled: serviceBtnEnabled.value)
- ]);
- });
+ return _Card(title: 'Service', children: [
+ Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
+ () async {
+ serviceBtnEnabled.value = false;
+ await start_service(serviceStop.value);
+ // enable the button after 1 second
+ Future.delayed(const Duration(seconds: 1), () {
+ serviceBtnEnabled.value = true;
+ });
+ }();
+ }, enabled: serviceBtnEnabled.value))
+ ]);
}
Widget other() {
@@ -488,16 +479,6 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
- if (!bind.isIncomingOnly())
- _OptionCheckBox(
- context,
- 'allow-remote-toolbar-docking-any-edge',
- kOptionAllowMultiEdgeToolbarDock,
- isServer: false,
- update: (_) {
- reloadAllWindows();
- },
- ),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[
@@ -1072,10 +1053,6 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(context, 'Enable blocking user input',
kOptionEnableBlockInput,
enabled: enabled, fakeValue: fakeValue),
- if (bind.mainSupportedPrivacyModeImpls() != '[]')
- _OptionCheckBox(
- context, 'Enable privacy mode', kOptionEnablePrivacyMode,
- enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification',
kOptionAllowRemoteConfigModification,
enabled: enabled, fakeValue: fakeValue),
@@ -1123,9 +1100,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
- (await bind.mainGetCommon(
- key: "permanent-password-set")) !=
- "true") {
+ (await bind.mainGetPermanentPassword())
+ .isEmpty) {
if (isChangePermanentPasswordDisabled()) {
await callback();
return;
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 8bd7df08b..7d48452a8 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -610,24 +610,19 @@ class _PrivilegeBoard extends StatefulWidget {
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
late final client = widget.client;
Widget buildPermissionIcon(bool enabled, IconData iconData,
- Function(bool)? onTap, String tooltipText,
- {required bool canModify}) {
+ Function(bool)? onTap, String tooltipText) {
return Tooltip(
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
waitDuration: Duration.zero,
child: Container(
decoration: BoxDecoration(
- color: enabled
- ? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6))
- : Colors.grey[700],
+ color: enabled ? MyTheme.accent : Colors.grey[700],
borderRadius: BorderRadius.circular(10.0),
),
padding: EdgeInsets.all(8.0),
child: InkWell(
- onTap: canModify
- ? () =>
- checkClickTime(widget.client.id, () => onTap?.call(!enabled))
- : null,
+ onTap: () =>
+ checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -648,9 +643,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
Widget build(BuildContext context) {
final crossAxisCount = 4;
final spacing = 10.0;
- final canModifyPermission =
- bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) !=
- 'N';
return Container(
width: double.infinity,
height: 160.0,
@@ -697,7 +689,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -712,7 +703,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
- canModify: canModifyPermission,
),
]
: [
@@ -729,7 +719,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable keyboard/mouse'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.clipboard,
@@ -744,7 +733,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable clipboard'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.audio,
@@ -759,7 +747,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.file,
@@ -774,7 +761,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable file copy and paste'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.restart,
@@ -789,7 +775,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable remote restart'),
- canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -804,7 +789,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
- canModify: canModifyPermission,
),
// only windows support block input
if (isWindows)
@@ -821,23 +805,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable blocking user input'),
- canModify: canModifyPermission,
- ),
- if (bind.mainSupportedPrivacyModeImpls() != '[]')
- buildPermissionIcon(
- client.privacyMode,
- Icons.visibility_off,
- (enabled) {
- bind.cmSwitchPermission(
- connId: client.id,
- name: "privacy_mode",
- enabled: enabled);
- setState(() {
- client.privacyMode = enabled;
- });
- },
- translate('Enable privacy mode'),
- canModify: canModifyPermission,
)
],
),
diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart
index d38dc4a8b..0070cd73b 100644
--- a/flutter/lib/desktop/pages/terminal_page.dart
+++ b/flutter/lib/desktop/pages/terminal_page.dart
@@ -27,7 +27,6 @@ class TerminalPage extends StatefulWidget {
final bool? isSharedPassword;
final String? connToken;
final int terminalId;
-
/// Tab key for focus management, passed from parent to avoid duplicate construction
final String tabKey;
final SimpleWrapper?> _lastState = SimpleWrapper(null);
@@ -44,9 +43,6 @@ class TerminalPage extends StatefulWidget {
class _TerminalPageState extends State
with AutomaticKeepAliveClientMixin {
- static const EdgeInsets _defaultTerminalPadding =
- EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
-
late FFI _ffi;
late TerminalModel _terminalModel;
double? _cellHeight;
@@ -159,27 +155,13 @@ class _TerminalPageState extends State
// extra space left after dividing the available height by the height of a single
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
EdgeInsets _calculatePadding(double heightPx) {
- final cellHeight = _cellHeight;
- if (!heightPx.isFinite ||
- heightPx <= 0 ||
- cellHeight == null ||
- !cellHeight.isFinite ||
- cellHeight <= 0) {
- return _defaultTerminalPadding;
- }
- final rows = (heightPx / cellHeight).floor();
- if (rows <= 0) {
- return _defaultTerminalPadding;
- }
- final extraSpace = heightPx - rows * cellHeight;
- if (!extraSpace.isFinite || extraSpace < 0) {
- return _defaultTerminalPadding;
+ if (_cellHeight == null) {
+ return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
}
+ final rows = (heightPx / _cellHeight!).floor();
+ final extraSpace = heightPx - rows * _cellHeight!;
final topBottom = extraSpace / 2.0;
- return EdgeInsets.symmetric(
- horizontal: _defaultTerminalPadding.horizontal / 2,
- vertical: topBottom,
- );
+ return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
}
@override
diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart
index 63289e94d..28e59fb05 100644
--- a/flutter/lib/desktop/pages/terminal_tab_page.dart
+++ b/flutter/lib/desktop/pages/terminal_tab_page.dart
@@ -46,7 +46,6 @@ class _TerminalTabPageState extends State {
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
- tabController.onCloseWindow = _closeWindowFromConnection;
final terminalId = params['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab(
peerId: params['id'],
@@ -145,8 +144,6 @@ class _TerminalTabPageState extends State {
_windowClosing = true;
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
- // Keep the cleanup target lookup below synchronous before its first await:
- // it relies on the current frame still retaining each TerminalPage's FFI/model.
tabController.clear();
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
@@ -371,34 +368,8 @@ class _TerminalTabPageState extends State {
final persistentSessions =
args['persistent_sessions'] as List? ?? [];
final sortedSessions = persistentSessions.whereType().toList()..sort();
- var peerId = args['peer_id'] as String? ?? '';
- if (peerId.isEmpty) {
- if (tabController.state.value.tabs.isEmpty ||
- tabController.state.value.selected >=
- tabController.state.value.tabs.length) {
- debugPrint('[TerminalTabPage] Skip restore: no selected tab');
- return;
- }
- final currentTab = tabController.state.value.selectedTabInfo;
- final parsed = _parseTabKey(currentTab.key);
- if (parsed == null) return;
- peerId = parsed.$1;
- }
- final existingTerminalIds = tabController.state.value.tabs
- .map((tab) => _parseTabKey(tab.key))
- .where((parsed) => parsed != null && parsed.$1 == peerId)
- .map((parsed) => parsed!.$2)
- .toSet();
- if (existingTerminalIds.isEmpty) {
- debugPrint(
- '[TerminalTabPage] Skip restore: no seed tab for peer $peerId');
- return;
- }
for (final terminalId in sortedSessions) {
- if (!existingTerminalIds.add(terminalId)) {
- continue;
- }
- _addNewTerminal(peerId, terminalId: terminalId);
+ _addNewTerminalForCurrentPeer(terminalId: terminalId);
// A delay is required to ensure the UI has sufficient time to update
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
// may be called prematurely while the tab widget is still in the tab controller.
@@ -575,11 +546,6 @@ class _TerminalTabPageState extends State {
}
}
- Future _closeWindowFromConnection() async {
- await _closeAllTabs();
- await WindowController.fromWindowId(windowId()).close();
- }
-
int windowId() {
return widget.params["windowId"];
}
diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart
index 44a2dc1c7..ec05c987f 100644
--- a/flutter/lib/desktop/widgets/remote_toolbar.dart
+++ b/flutter/lib/desktop/widgets/remote_toolbar.dart
@@ -28,220 +28,6 @@ import './kb_layout_type_chooser.dart';
import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
-enum _ToolbarEdge { top, right, bottom, left }
-
-_ToolbarEdge _parseToolbarEdge(String? s) {
- switch (s) {
- case 'right':
- return _ToolbarEdge.right;
- case 'bottom':
- return _ToolbarEdge.bottom;
- case 'left':
- return _ToolbarEdge.left;
- default:
- return _ToolbarEdge.top;
- }
-}
-
-String _toolbarEdgeToString(_ToolbarEdge e) {
- switch (e) {
- case _ToolbarEdge.top:
- return 'top';
- case _ToolbarEdge.right:
- return 'right';
- case _ToolbarEdge.bottom:
- return 'bottom';
- case _ToolbarEdge.left:
- return 'left';
- }
-}
-
-bool _isHorizontalEdge(_ToolbarEdge e) =>
- e == _ToolbarEdge.top || e == _ToolbarEdge.bottom;
-
-const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x';
-
-double _clampToolbarFraction(double fraction, double left, double right) {
- if (fraction < left) fraction = left;
- if (fraction > right) fraction = right;
- return fraction;
-}
-
-Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) {
- final isHorizontal = _isHorizontalEdge(edge);
- final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360);
- final size = measured ?? fallback;
- final long = size.longestSide;
- final short = size.shortestSide;
- return Size(isHorizontal ? long : short, isHorizontal ? short : long);
-}
-
-Offset _toolbarOffsetForEdge({
- required _ToolbarEdge edge,
- required double fraction,
- required Size parentSize,
- required Size toolbarSize,
-}) {
- final xTravel = parentSize.width - toolbarSize.width;
- final yTravel = parentSize.height - toolbarSize.height;
- switch (edge) {
- case _ToolbarEdge.top:
- return Offset(xTravel * fraction, 0);
- case _ToolbarEdge.bottom:
- return Offset(xTravel * fraction, yTravel);
- case _ToolbarEdge.left:
- return Offset(0, yTravel * fraction);
- case _ToolbarEdge.right:
- return Offset(xTravel, yTravel * fraction);
- }
-}
-
-double _fractionForAlignedDrag({
- required double cursor,
- required double grabOffset,
- required double parentExtent,
- required double toolbarExtent,
- required double left,
- required double right,
-}) {
- final travelExtent = parentExtent - toolbarExtent;
- if (travelExtent <= 0) {
- return _clampToolbarFraction(0.5, left, right);
- }
- return _clampToolbarFraction(
- (cursor - grabOffset) / travelExtent, left, right);
-}
-
-({double left, double right}) _fractionBoundsForEdge(
- _ToolbarEdge edge,
- double left,
- double right,
-) {
- return _isHorizontalEdge(edge)
- ? (left: left, right: right)
- : (left: 0, right: 1);
-}
-
-String _toolbarRawFraction({
- required bool multiEdgeEnabled,
- required _ToolbarEdge edge,
- required String? savedFraction,
- required String? legacyFraction,
-}) {
- if (!multiEdgeEnabled) {
- return (legacyFraction != null && legacyFraction.isNotEmpty)
- ? legacyFraction
- : '0.5';
- }
- if (savedFraction != null && savedFraction.isNotEmpty) {
- return savedFraction;
- }
- if (edge == _ToolbarEdge.top &&
- legacyFraction != null &&
- legacyFraction.isNotEmpty) {
- return legacyFraction;
- }
- return '0.5';
-}
-
-// Returns the alignment for the wrapper Align that positions the entire
-// toolbar against the given edge at the given fraction along that edge.
-// Alignment uses [-1, 1] coordinates (0 = center).
-Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) {
- final f = fraction * 2 - 1;
- switch (edge) {
- case _ToolbarEdge.top:
- return Alignment(f, -1);
- case _ToolbarEdge.bottom:
- return Alignment(f, 1);
- case _ToolbarEdge.left:
- return Alignment(-1, f);
- case _ToolbarEdge.right:
- return Alignment(1, f);
- }
-}
-
-// The drag handle hangs off the side of the toolbar facing away from the
-// docked edge, so the icons themselves sit flush against that edge.
-BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) {
- const r = Radius.circular(5);
- switch (edge) {
- case _ToolbarEdge.top:
- return const BorderRadius.vertical(bottom: r);
- case _ToolbarEdge.bottom:
- return const BorderRadius.vertical(top: r);
- case _ToolbarEdge.left:
- return const BorderRadius.horizontal(right: r);
- case _ToolbarEdge.right:
- return const BorderRadius.horizontal(left: r);
- }
-}
-
-int _monitorMenuQuarterTurns(_ToolbarEdge edge) {
- switch (edge) {
- case _ToolbarEdge.left:
- return 1;
- case _ToolbarEdge.right:
- return 3;
- case _ToolbarEdge.top:
- case _ToolbarEdge.bottom:
- return 0;
- }
-}
-
-IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) {
- switch (edge) {
- case _ToolbarEdge.top:
- return isCollapsed ? Icons.expand_more : Icons.expand_less;
- case _ToolbarEdge.bottom:
- return isCollapsed ? Icons.expand_less : Icons.expand_more;
- case _ToolbarEdge.left:
- return isCollapsed ? Icons.chevron_right : Icons.chevron_left;
- case _ToolbarEdge.right:
- return isCollapsed ? Icons.chevron_left : Icons.chevron_right;
- }
-}
-
-class _ToolbarDockingOptions {
- _ToolbarDockingOptions({
- required this.edge,
- required this.fraction,
- required this.multiEdgeEnabled,
- });
-
- _ToolbarEdge edge;
- double fraction;
- bool multiEdgeEnabled;
-}
-
-final _toolbarDockingOptionsBySession = {};
-
-String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString();
-
-_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) =>
- _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)];
-
-void _cacheToolbarDockingOptions({
- required SessionID sessionId,
- required _ToolbarEdge edge,
- required double fraction,
- required bool multiEdgeEnabled,
-}) {
- final key = _toolbarDockingCacheKey(sessionId);
- final cached = _toolbarDockingOptionsBySession[key];
- if (cached == null) {
- _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions(
- edge: edge,
- fraction: fraction,
- multiEdgeEnabled: multiEdgeEnabled,
- );
- return;
- }
- cached.edge = edge;
- cached.fraction = fraction;
- cached.multiEdgeEnabled = multiEdgeEnabled;
-}
-
class ToolbarState {
late RxBool _pin;
@@ -464,26 +250,8 @@ class RemoteToolbar extends StatefulWidget {
class _RemoteToolbarState extends State {
late Debouncer _debouncerHide;
bool _isCursorOverImage = false;
- final _fraction = 0.5.obs;
- final _edge = _ToolbarEdge.top.obs;
+ final _fractionX = 0.5.obs;
final _dragging = false.obs;
- // Live drag preview: where the toolbar would dock if the user dropped now.
- final _previewEdge = Rxn<_ToolbarEdge>();
- final _previewFraction = Rxn();
- // Measured size of the live toolbar, so the preview ghost matches reality
- // (collapsed handle vs expanded toolbar). Updated after every layout pass.
- final _toolbarSize = Rxn();
- final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root');
- // When false (default), the toolbar stays on the top edge and the drag
- // handle just slides it horizontally — preserving long-standing UX while
- // still fixing the bug where dragging only moved the handle. When true,
- // the user has opted into multi-edge docking with nearest-edge snap.
- // Kept in sync after settings-triggered rebuilds.
- final _multiEdgeEnabled = false.obs;
- final _dockingOptionsInitialized = false.obs;
- bool _pendingDockingOptionSync = false;
- int _dockingOptionSyncSerial = 0;
- int _dragEpoch = 0;
int get windowId => stateGlobal.windowId;
@@ -505,144 +273,16 @@ class _RemoteToolbarState extends State {
void _minimize() async =>
await WindowController.fromWindowId(windowId).minimize();
- Future _syncDockingOptions({required bool force}) async {
- final syncSerial = ++_dockingOptionSyncSerial;
- if (_dragging.isTrue) {
- _deferDockingOptionsSync();
- return;
- }
- final dragEpoch = _dragEpoch;
-
- // Use the canonical helper so the option's documented default semantics
- // apply (allow-* prefix => default false). Keeping it raw-string would
- // diverge from how _OptionCheckBox displays the same key.
- final multiEdgeEnabled =
- mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
- final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
- if (cached == null && pi.isSet.isFalse) {
- return;
- }
- final hadDockingOptions = cached != null;
- final wasMultiEdgeEnabled =
- cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value;
- if (!force &&
- hadDockingOptions &&
- wasMultiEdgeEnabled == multiEdgeEnabled) {
- _pendingDockingOptionSync = false;
- return;
- }
-
- final savedFraction = await bind.sessionGetOption(
- sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction);
- // Backward compat: legacy horizontal-only position.
- final legacyFraction = await bind.sessionGetOption(
- sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX);
- if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
-
- var nextEdge = _edge.value;
- var savedFractionForNextEdge = savedFraction;
- var keepCurrentPosition = false;
- if (!multiEdgeEnabled) {
- nextEdge = _ToolbarEdge.top;
- } else if (force || wasMultiEdgeEnabled || cached == null) {
- final edgeStr = await bind.sessionGetOption(
- sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge);
- if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
- nextEdge = _parseToolbarEdge(edgeStr);
- } else {
- // The setting changed from top-only to multi-edge while this toolbar is
- // already visible. Keep its current position instead of jumping to the
- // last saved multi-edge dock.
- nextEdge = cached.edge;
- savedFractionForNextEdge = cached.fraction.toString();
- keepCurrentPosition = true;
- }
-
- final rawFraction = _toolbarRawFraction(
- multiEdgeEnabled: multiEdgeEnabled,
- edge: nextEdge,
- savedFraction: savedFractionForNextEdge,
- legacyFraction: legacyFraction,
- );
- // Clamp to the saved drag-bound contract so a corrupted or out-of-range
- // saved value can't bypass it until the user drags again.
- final dragLeft = double.tryParse(
- bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ??
- 0.0;
- final dragRight = double.tryParse(
- bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ??
- 1.0;
- final fractionBounds =
- _fractionBoundsForEdge(nextEdge, dragLeft, dragRight);
- final nextFraction = (double.tryParse(rawFraction) ?? 0.5)
- .clamp(fractionBounds.left, fractionBounds.right)
- .toDouble();
- if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
- if (_dragging.isTrue || dragEpoch != _dragEpoch) {
- _deferDockingOptionsSync();
- return;
- }
- _edge.value = nextEdge;
- _fraction.value = nextFraction;
- _multiEdgeEnabled.value = multiEdgeEnabled;
- _dockingOptionsInitialized.value = true;
- _cacheToolbarDockingOptions(
- sessionId: widget.ffi.sessionId,
- edge: nextEdge,
- fraction: nextFraction,
- multiEdgeEnabled: multiEdgeEnabled,
- );
- _pendingDockingOptionSync = false;
- if (!multiEdgeEnabled || keepCurrentPosition) {
- bind.sessionPeerOption(
- sessionId: widget.ffi.sessionId,
- name: kOptionRemoteMenubarEdge,
- value: _toolbarEdgeToString(nextEdge),
- );
- bind.sessionPeerOption(
- sessionId: widget.ffi.sessionId,
- name: kOptionRemoteMenubarFraction,
- value: nextFraction.toString(),
- );
- }
- }
-
- void _deferDockingOptionsSync() {
- _pendingDockingOptionSync = true;
- if (_dragging.isFalse) {
- _syncDockingOptionsAfterDragIfNeeded();
- }
- }
-
- void _markToolbarDragEpoch() {
- ++_dragEpoch;
- }
-
- void _syncDockingOptionsAfterDragIfNeeded() {
- if (!_pendingDockingOptionSync) return;
- WidgetsBinding.instance.addPostFrameCallback((_) async {
- await _syncDockingOptions(force: false);
- });
- }
-
@override
initState() {
super.initState();
- final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
- final multiEdgeEnabled =
- mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
- final shouldResetToTop =
- cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled;
- if (cached != null && !shouldResetToTop) {
- _edge.value = cached.edge;
- _fraction.value = cached.fraction;
- _multiEdgeEnabled.value = multiEdgeEnabled;
- _dockingOptionsInitialized.value = true;
- }
-
WidgetsBinding.instance.addPostFrameCallback((_) async {
- await _syncDockingOptions(force: cached == null || shouldResetToTop);
+ _fractionX.value = double.tryParse(await bind.sessionGetOption(
+ sessionId: widget.ffi.sessionId,
+ arg: 'remote-menubar-drag-x') ??
+ '0.5') ??
+ 0.5;
// Initialize toolbar states (collapse, hide) from session options
widget.state.init(widget.ffi.sessionId);
});
@@ -663,14 +303,6 @@ class _RemoteToolbarState extends State {
});
}
- @override
- void didUpdateWidget(covariant RemoteToolbar oldWidget) {
- super.didUpdateWidget(oldWidget);
- WidgetsBinding.instance.addPostFrameCallback((_) async {
- await _syncDockingOptions(force: false);
- });
- }
-
_debouncerHideProc(int v) {
if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) {
collapse.value = true;
@@ -679,130 +311,64 @@ class _RemoteToolbarState extends State {
@override
dispose() {
- ++_dockingOptionSyncSerial;
- widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
super.dispose();
+
+ widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
}
@override
Widget build(BuildContext context) {
return Obx(() {
// Wait for initialization to complete to prevent flickering
- if (!widget.state.initialized.value ||
- !_dockingOptionsInitialized.value) {
+ if (!widget.state.initialized.value) {
return const SizedBox.shrink();
}
// If toolbar is hidden, return empty widget
if (hide.value) {
return const SizedBox.shrink();
}
- final edge = _edge.value;
- final isHorizontal = _isHorizontalEdge(edge);
-
- // Measure the live toolbar after every layout so the preview ghost can
- // match its actual footprint (collapsed handle vs expanded toolbar).
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (_dragging.isTrue) return;
- final ro = _toolbarKey.currentContext?.findRenderObject();
- if (ro is RenderBox && ro.hasSize) {
- final s = ro.size;
- if (_toolbarSize.value != s) _toolbarSize.value = s;
- }
- });
-
- final toolbar = Align(
- alignment: _alignmentForEdge(edge, _fraction.value),
- child: KeyedSubtree(
- key: _toolbarKey,
- child: collapse.isFalse
- ? _buildToolbar(context, edge, isHorizontal)
- : _buildDraggableCollapse(context, edge, isHorizontal),
- ),
- );
-
- // Always return the Stack — even when not dragging — so the toolbar's
- // position in the Element tree stays stable. Wrapping/unwrapping it
- // mid-drag was killing the Draggable's gesture state.
- return Stack(
- fit: StackFit.expand,
- children: [
- IgnorePointer(
- child: Obx(() {
- final pe = _previewEdge.value;
- final pf = _previewFraction.value;
- if (!_dragging.isTrue || pe == null || pf == null) {
- return const SizedBox.shrink();
- }
- return _buildDragPreview(context, pe, pf, _toolbarSize.value);
- }),
- ),
- toolbar,
- ],
+ return Align(
+ alignment: Alignment.topCenter,
+ child: collapse.isFalse
+ ? _buildToolbar(context)
+ : _buildDraggableCollapse(context),
);
});
}
- Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge,
- double fraction, Size? measured) {
- final color = Theme.of(context).colorScheme.primary;
- // Use the measured live toolbar size so collapsed vs expanded looks
- // right. The current orientation may differ from the preview orientation
- // (e.g. dragging a top-docked toolbar toward the left edge), so swap the
- // long/short axes when previewing a different orientation.
- final previewSize = _toolbarSizeForEdge(edge, measured);
- return Align(
- alignment: _alignmentForEdge(edge, fraction),
- child: Container(
- width: previewSize.width,
- height: previewSize.height,
- decoration: BoxDecoration(
- color: color.withOpacity(0.10),
- borderRadius: BorderRadius.circular(6),
- border: Border.all(color: color.withOpacity(0.55), width: 1.5),
- ),
- ),
- );
- }
-
- Widget _buildDraggableCollapse(
- BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
+ Widget _buildDraggableCollapse(BuildContext context) {
return Obx(() {
if (collapse.isFalse && _dragging.isFalse) {
triggerAutoHide();
}
- final borderRadius = _collapseHandleBorderRadius(edge);
- return Offstage(
- offstage: _dragging.isTrue,
- child: Material(
- elevation: _ToolbarTheme.elevation,
- shadowColor: MyTheme.color(context).shadow,
- borderRadius: borderRadius,
- child: _DraggableShowHide(
- id: widget.id,
- sessionId: widget.ffi.sessionId,
- dragging: _dragging,
- fraction: _fraction,
- edge: _edge,
- previewEdge: _previewEdge,
- previewFraction: _previewFraction,
- toolbarSize: _toolbarSize,
- markDragEpoch: _markToolbarDragEpoch,
- syncDockingOptionsAfterDragIfNeeded:
- _syncDockingOptionsAfterDragIfNeeded,
- isHorizontal: isHorizontal,
- multiEdgeEnabled: _multiEdgeEnabled.value,
- toolbarState: widget.state,
- setFullscreen: _setFullscreen,
- setMinimize: _minimize,
+ final borderRadius = BorderRadius.vertical(
+ bottom: Radius.circular(5),
+ );
+ return Align(
+ alignment: FractionalOffset(_fractionX.value, 0),
+ child: Offstage(
+ offstage: _dragging.isTrue,
+ child: Material(
+ elevation: _ToolbarTheme.elevation,
+ shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius,
+ child: _DraggableShowHide(
+ id: widget.id,
+ sessionId: widget.ffi.sessionId,
+ dragging: _dragging,
+ fractionX: _fractionX,
+ toolbarState: widget.state,
+ setFullscreen: _setFullscreen,
+ setMinimize: _minimize,
+ borderRadius: borderRadius,
+ ),
),
),
);
});
}
- Widget _buildToolbar(
- BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
+ Widget _buildToolbar(BuildContext context) {
final List toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) {
@@ -810,13 +376,11 @@ class _RemoteToolbarState extends State {
}
toolbarItems.add(Obx(() {
- if ((PrivacyModeState.find(widget.id).isEmpty ||
- allowDisplaySwitchInPrivacyMode(pi)) &&
+ if (PrivacyModeState.find(widget.id).isEmpty &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,
ffi: widget.ffi,
- edge: edge,
setRemoteState: widget.setRemoteState);
} else {
return Offstage();
@@ -842,53 +406,37 @@ class _RemoteToolbarState extends State {
if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
- // innerAxis: how the toolbar icons themselves flow.
- // outerAxis: how the toolbar block and the handle stack against each other
- // (perpendicular to the dock edge, so the handle hangs off the interior face).
- final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical;
- final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal;
- final spacer = isHorizontal
- ? SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
- : SizedBox(height: _ToolbarTheme.buttonHMargin * 2);
- final toolbarMaterial = Material(
- elevation: _ToolbarTheme.elevation,
- shadowColor: MyTheme.color(context).shadow,
- borderRadius: toolbarBorderRadius,
- color: Theme.of(context)
- .menuBarTheme
- .style
- ?.backgroundColor
- ?.resolve(MaterialState.values.toSet()),
- child: SingleChildScrollView(
- scrollDirection: innerAxis,
- child: Theme(
- data: themeData(),
- child: _ToolbarTheme.borderWrapper(
- context,
- Flex(
- direction: innerAxis,
- mainAxisSize: MainAxisSize.min,
- children: [
- spacer,
- ...toolbarItems,
- spacer,
- ],
- ),
- toolbarBorderRadius),
- ),
- ),
- );
- final handle = _buildDraggableCollapse(context, edge, isHorizontal);
- // The handle hangs off the interior face of the toolbar (away from the
- // docked edge), centered along that face by the Flex's default cross-axis
- // alignment, so the icons themselves sit flush against the docked edge.
- final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left)
- ? [toolbarMaterial, handle]
- : [handle, toolbarMaterial];
- return Flex(
- direction: outerAxis,
+ return Column(
mainAxisSize: MainAxisSize.min,
- children: children,
+ children: [
+ Material(
+ elevation: _ToolbarTheme.elevation,
+ shadowColor: MyTheme.color(context).shadow,
+ borderRadius: toolbarBorderRadius,
+ color: Theme.of(context)
+ .menuBarTheme
+ .style
+ ?.backgroundColor
+ ?.resolve(MaterialState.values.toSet()),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Theme(
+ data: themeData(),
+ child: _ToolbarTheme.borderWrapper(
+ context,
+ Row(
+ children: [
+ SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
+ ...toolbarItems,
+ SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
+ ],
+ ),
+ toolbarBorderRadius),
+ ),
+ ),
+ ),
+ _buildDraggableCollapse(context),
+ ],
);
}
@@ -967,13 +515,11 @@ class _MobileActionMenu extends StatelessWidget {
class _MonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
- final _ToolbarEdge edge;
final Function(VoidCallback) setRemoteState;
const _MonitorMenu({
Key? key,
required this.id,
required this.ffi,
- required this.edge,
required this.setRemoteState,
}) : super(key: key);
@@ -984,17 +530,9 @@ class _MonitorMenu extends StatelessWidget {
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
@override
- Widget build(BuildContext context) {
- final child = showMonitorsToolbar
- ? buildMultiMonitorMenu(context)
- : Obx(() => buildMonitorMenu(context));
- final quarterTurns = _monitorMenuQuarterTurns(edge);
- if (quarterTurns == 0) return child;
- return RotatedBox(
- quarterTurns: quarterTurns,
- child: child,
- );
- }
+ Widget build(BuildContext context) => showMonitorsToolbar
+ ? buildMultiMonitorMenu(context)
+ : Obx(() => buildMonitorMenu(context));
Widget buildMonitorMenu(BuildContext context) {
final width = SimpleWrapper(0);
@@ -1126,8 +664,7 @@ class _MonitorMenu extends StatelessWidget {
}
final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
- final height = rect.height * scale;
- final startY = (_ToolbarTheme.buttonSize - height) * 0.5;
+ final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
final startX = startY;
final children = [];
@@ -1170,7 +707,7 @@ class _MonitorMenu extends StatelessWidget {
width.value = rect.width * scale + startX * 2;
return SizedBox(
width: width.value,
- height: height + startY * 2,
+ height: rect.height * scale + startY * 2,
child: Stack(
children: children,
),
@@ -1459,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
toggles(),
];
// privacy mode
- final privacyModeState = PrivacyModeState.find(id);
if (ffi.connType == ConnType.defaultConn &&
- (pi.features.privacyMode || privacyModeState.isNotEmpty) &&
- (ffiModel.keyboard || privacyModeState.isNotEmpty)) {
+ ffiModel.keyboard &&
+ pi.features.privacyMode) {
+ final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
if (privacyModeList.length == 1) {
@@ -2981,18 +2518,7 @@ class RdoMenuButton extends StatelessWidget {
class _DraggableShowHide extends StatefulWidget {
final String id;
final SessionID sessionId;
- final RxDouble fraction;
- final Rx<_ToolbarEdge> edge;
- final Rxn<_ToolbarEdge> previewEdge;
- final Rxn previewFraction;
- final Rxn toolbarSize;
- final VoidCallback markDragEpoch;
- final VoidCallback syncDockingOptionsAfterDragIfNeeded;
- final bool isHorizontal;
- // Whether multi-edge docking is enabled for this session (toggled in
- // Settings -> Other). When false, the drag handle slides the toolbar
- // horizontally on the top edge and never switches edges.
- final bool multiEdgeEnabled;
+ final RxDouble fractionX;
final RxBool dragging;
final ToolbarState toolbarState;
final BorderRadius borderRadius;
@@ -3004,15 +2530,7 @@ class _DraggableShowHide extends StatefulWidget {
Key? key,
required this.id,
required this.sessionId,
- required this.fraction,
- required this.edge,
- required this.previewEdge,
- required this.previewFraction,
- required this.toolbarSize,
- required this.markDragEpoch,
- required this.syncDockingOptionsAfterDragIfNeeded,
- required this.isHorizontal,
- required this.multiEdgeEnabled,
+ required this.fractionX,
required this.dragging,
required this.toolbarState,
required this.setFullscreen,
@@ -3025,12 +2543,10 @@ class _DraggableShowHide extends StatefulWidget {
}
class _DraggableShowHideState extends State<_DraggableShowHide> {
+ Offset position = Offset.zero;
+ Size size = Size.zero;
double left = 0.0;
double right = 1.0;
- Offset? _lastPointerDown;
- Offset? _dragGrabOffset;
- double? _dragLongAxisGrabOffset;
- Size? _dragToolbarSize;
RxBool get collapse => widget.toolbarState.collapse;
@@ -3056,174 +2572,41 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
}
}
- // Bias applied to the currently-previewed edge so a drag hovering between
- // two edges doesn't flicker. Only relevant when multi-edge is enabled.
- static const double _switchHysteresisPx = 50.0;
-
- _ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) {
- if (!widget.multiEdgeEnabled) return widget.edge.value;
-
- double rawDist(_ToolbarEdge e) {
- switch (e) {
- case _ToolbarEdge.top:
- return cursor.dy;
- case _ToolbarEdge.bottom:
- return mediaSize.height - cursor.dy;
- case _ToolbarEdge.left:
- return cursor.dx;
- case _ToolbarEdge.right:
- return mediaSize.width - cursor.dx;
- }
- }
-
- final previewed = widget.previewEdge.value;
- var winner = widget.edge.value;
- var best = double.infinity;
- for (final e in _ToolbarEdge.values) {
- final biased =
- e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e);
- if (biased < best) {
- best = biased;
- winner = e;
- }
- }
- return winner;
- }
-
- void _ensureDragGrabOffset(Offset cursor) {
- if (_dragGrabOffset != null) return;
- final mediaSize = MediaQueryData.fromView(View.of(context)).size;
- final toolbarSize =
- _toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value);
- _dragToolbarSize = toolbarSize;
- final toolbarOffset = _toolbarOffsetForEdge(
- edge: widget.edge.value,
- fraction: widget.fraction.value,
- parentSize: mediaSize,
- toolbarSize: toolbarSize,
- );
- _dragGrabOffset = cursor - toolbarOffset;
- _dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value)
- ? _dragGrabOffset?.dx
- : _dragGrabOffset?.dy;
- }
-
- double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) {
- final offset = _dragLongAxisGrabOffset ?? 0;
- final extent =
- _isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height;
- return _clampToolbarFraction(offset, 0, extent);
- }
-
- void _updatePreview(Offset cursor) {
- _ensureDragGrabOffset(cursor);
- final mediaSize = MediaQueryData.fromView(View.of(context)).size;
- final winner = _nearestToolbarEdge(cursor, mediaSize);
- widget.previewEdge.value = winner;
-
- final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize);
- final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize);
- final double frac;
- if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) {
- frac = _fractionForAlignedDrag(
- cursor: cursor.dx,
- grabOffset: grabOffset,
- parentExtent: mediaSize.width,
- toolbarExtent: toolbarSize.width,
- left: left,
- right: right,
- );
- } else {
- final fractionBounds = _fractionBoundsForEdge(winner, left, right);
- frac = _fractionForAlignedDrag(
- cursor: cursor.dy,
- grabOffset: grabOffset,
- parentExtent: mediaSize.height,
- toolbarExtent: toolbarSize.height,
- left: fractionBounds.left,
- right: fractionBounds.right,
- );
- }
- widget.previewFraction.value = frac;
- }
-
- void _resetDragTracking() {
- _lastPointerDown = null;
- _dragGrabOffset = null;
- _dragLongAxisGrabOffset = null;
- _dragToolbarSize = null;
- }
-
- void _commitPreview() {
- final newEdge = widget.previewEdge.value;
- final frac = widget.previewFraction.value;
- widget.previewEdge.value = null;
- widget.previewFraction.value = null;
- widget.dragging.value = false;
- widget.markDragEpoch();
- _resetDragTracking();
- widget.syncDockingOptionsAfterDragIfNeeded();
- if (newEdge == null || frac == null) return;
- widget.edge.value = newEdge;
- widget.fraction.value = frac;
- _cacheToolbarDockingOptions(
- sessionId: widget.sessionId,
- edge: newEdge,
- fraction: frac,
- multiEdgeEnabled: widget.multiEdgeEnabled,
- );
- bind.sessionPeerOption(
- sessionId: widget.sessionId,
- name: kOptionRemoteMenubarEdge,
- value: _toolbarEdgeToString(newEdge),
- );
- bind.sessionPeerOption(
- sessionId: widget.sessionId,
- name: kOptionRemoteMenubarFraction,
- value: frac.toString(),
- );
- if (widget.multiEdgeEnabled) {
- return;
- }
- bind.sessionPeerOption(
- sessionId: widget.sessionId,
- name: _legacyRemoteMenubarDragX,
- value: frac.toString(),
- );
- }
-
Widget _buildDraggable(BuildContext context) {
- return Listener(
- onPointerDown: (event) => _lastPointerDown = event.position,
- child: Draggable(
- // When multi-edge docking is off the toolbar stays on the top edge,
- // so lock the feedback to horizontal motion — otherwise the handle
- // floats away from the top while dragging and the toolbar looks
- // unmoored. When multi-edge is on we need 2D drag for snap-to-edge.
- axis: widget.multiEdgeEnabled ? null : Axis.horizontal,
- child: Icon(
- widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle,
- size: 20,
- color: MyTheme.color(context).drag_indicator,
- ),
- feedback: widget,
- onDragStarted: () {
- widget.markDragEpoch();
- final pointerDown = _lastPointerDown;
- if (pointerDown != null) {
- _ensureDragGrabOffset(pointerDown);
- }
- widget.dragging.value = true;
- // Seed the preview at the current docked edge/fraction so something
- // shows the instant the drag begins, before the first onDragUpdate.
- widget.previewEdge.value = widget.edge.value;
- widget.previewFraction.value = widget.fraction.value;
- },
- onDragUpdate: (details) {
- _updatePreview(details.globalPosition);
- },
- onDragEnd: (_) => _commitPreview(),
+ return Draggable(
+ axis: Axis.horizontal,
+ child: Icon(
+ Icons.drag_indicator,
+ size: 20,
+ color: MyTheme.color(context).drag_indicator,
),
+ feedback: widget,
+ onDragStarted: (() {
+ final RenderObject? renderObj = context.findRenderObject();
+ if (renderObj != null) {
+ final RenderBox renderBox = renderObj as RenderBox;
+ size = renderBox.size;
+ position = renderBox.localToGlobal(Offset.zero);
+ }
+ widget.dragging.value = true;
+ }),
+ onDragEnd: (details) {
+ final mediaSize = MediaQueryData.fromView(View.of(context)).size;
+ widget.fractionX.value +=
+ (details.offset.dx - position.dx) / (mediaSize.width - size.width);
+ if (widget.fractionX.value < left) {
+ widget.fractionX.value = left;
+ }
+ if (widget.fractionX.value > right) {
+ widget.fractionX.value = right;
+ }
+ bind.sessionPeerOption(
+ sessionId: widget.sessionId,
+ name: 'remote-menubar-drag-x',
+ value: widget.fractionX.value.toString(),
+ );
+ widget.dragging.value = false;
+ },
);
}
@@ -3253,9 +2636,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
}
- final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical;
- final child = Flex(
- direction: axis,
+ final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
@@ -3296,7 +2677,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
message: translate(
collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
- _toolbarCollapseIcon(widget.edge.value, collapse.isTrue),
+ collapse.isFalse ? Icons.expand_less : Icons.expand_more,
size: iconSize,
),
))),
@@ -3338,8 +2719,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
borderRadius: widget.borderRadius,
),
child: SizedBox(
- height: widget.isHorizontal ? 20 : null,
- width: widget.isHorizontal ? null : 20,
+ height: 20,
child: child,
),
),
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index 9ef7d38d9..ac7d80017 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -99,7 +99,6 @@ class DesktopTabController {
/// index, key
Function(int, String)? onRemoved;
Function(String)? onSelected;
- Future Function()? onCloseWindow;
DesktopTabController(
{required this.tabType, this.onRemoved, this.onSelected});
@@ -593,13 +592,13 @@ class _DesktopTabState extends State
}
Widget _buildBar() {
- final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
return Row(
children: [
Expanded(
child: GestureDetector(
// custom double tap handler
- onTap: !isIncomingHomePage && showMaximize
+ onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
+ showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -610,7 +609,7 @@ class _DesktopTabState extends State
.then((value) => stateGlobal.setMaximized(value));
}
}
- : (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
+ : null,
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.
diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart
index 74a5af45c..9102d163c 100644
--- a/flutter/lib/mobile/pages/remote_page.dart
+++ b/flutter/lib/mobile/pages/remote_page.dart
@@ -426,10 +426,12 @@ class _RemotePageState extends State with WidgetsBindingObserver {
}
return Container(
color: MyTheme.canvasColor,
- child: RawTouchGestureDetectorRegion(
- child: getBodyForMobile(),
- ffi: gFFI,
- ),
+ child: inputModel.isPhysicalMouse.value
+ ? getBodyForMobile()
+ : RawTouchGestureDetectorRegion(
+ child: getBodyForMobile(),
+ ffi: gFFI,
+ ),
);
}),
),
@@ -1183,8 +1185,7 @@ void showOptions(
List privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
- if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
- privacyModeState.isNotEmpty) {
+ if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
if (privacyModeList.length == 1) {
displayToggles.add(privacyModeList[0]);
diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart
index cd3f97a53..54406ff2e 100644
--- a/flutter/lib/mobile/pages/server_page.dart
+++ b/flutter/lib/mobile/pages/server_page.dart
@@ -150,8 +150,7 @@ class _DropDownAction extends StatelessWidget {
}
if (value == kUsePermanentPassword &&
- (await bind.mainGetCommon(key: "permanent-password-set")) !=
- "true") {
+ (await bind.mainGetPermanentPassword()).isEmpty) {
if (isChangePermanentPasswordDisabled()) {
callback();
return;
@@ -583,20 +582,10 @@ class _PermissionCheckerState extends State {
Widget build(BuildContext context) {
final serverModel = Provider.of(context);
final hasAudioPermission = androidVersion >= 30;
- final hideStopService = isAndroid &&
- bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
- final allowPermChangeInAcceptWindow = option2bool(
- kOptionEnablePermChangeInAcceptWindow,
- bind.mainGetBuildinOption(
- key: kOptionEnablePermChangeInAcceptWindow,
- ));
- final permissionChangeLocked = isAndroid &&
- serverModel.clients.any((c) => !c.disconnected) &&
- !allowPermChangeInAcceptWindow;
return PaddingCard(
title: translate("Permissions"),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- serverModel.mediaOk && !hideStopService
+ serverModel.mediaOk
? ElevatedButton.icon(
style: ButtonStyle(
backgroundColor:
@@ -606,30 +595,21 @@ class _PermissionCheckerState extends State {
label: Text(translate("Stop service")))
.marginOnly(bottom: 8)
: SizedBox.shrink(),
- if (!hideStopService || !serverModel.mediaOk)
- PermissionRow(
- translate("Screen Capture"),
- serverModel.mediaOk,
- !serverModel.mediaOk &&
- gFFI.userModel.userName.value.isEmpty &&
- bind.mainGetLocalOption(key: "show-scam-warning") != "N"
- ? () => showScamWarning(context, serverModel)
- : serverModel.toggleService),
PermissionRow(
- translate("Input Control"),
- serverModel.inputOk,
- serverModel.toggleInput,
- ),
- PermissionRow(
- translate("Transfer file"),
- serverModel.fileOk,
- serverModel.toggleFile,
- enabled: !permissionChangeLocked,
- ),
+ translate("Screen Capture"),
+ serverModel.mediaOk,
+ !serverModel.mediaOk &&
+ gFFI.userModel.userName.value.isEmpty &&
+ bind.mainGetLocalOption(key: "show-scam-warning") != "N"
+ ? () => showScamWarning(context, serverModel)
+ : serverModel.toggleService),
+ PermissionRow(translate("Input Control"), serverModel.inputOk,
+ serverModel.toggleInput),
+ PermissionRow(translate("Transfer file"), serverModel.fileOk,
+ serverModel.toggleFile),
hasAudioPermission
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
- serverModel.toggleAudio,
- enabled: !permissionChangeLocked)
+ serverModel.toggleAudio)
: Row(children: [
Icon(Icons.info_outline).marginOnly(right: 15),
Expanded(
@@ -638,25 +618,19 @@ class _PermissionCheckerState extends State {
style: const TextStyle(color: MyTheme.darkGray),
))
]),
- PermissionRow(
- translate("Enable clipboard"),
- serverModel.clipboardOk,
- serverModel.toggleClipboard,
- enabled: !permissionChangeLocked,
- ),
+ PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
+ serverModel.toggleClipboard),
]));
}
}
class PermissionRow extends StatelessWidget {
- const PermissionRow(this.name, this.isOk, this.onPressed,
- {Key? key, this.enabled = true})
+ const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
: super(key: key);
final String name;
final bool isOk;
final VoidCallback onPressed;
- final bool enabled;
@override
Widget build(BuildContext context) {
@@ -665,11 +639,9 @@ class PermissionRow extends StatelessWidget {
contentPadding: EdgeInsets.all(0),
title: Text(name),
value: isOk,
- onChanged: enabled
- ? (bool value) {
- onPressed();
- }
- : null);
+ onChanged: (bool value) {
+ onPressed();
+ });
}
}
diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart
index 08c8cda1a..0898125c4 100644
--- a/flutter/lib/mobile/pages/view_camera_page.dart
+++ b/flutter/lib/mobile/pages/view_camera_page.dart
@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State
}
return Container(
color: MyTheme.canvasColor,
- child: RawTouchGestureDetectorRegion(
- child: getBodyForMobile(),
- ffi: gFFI,
- isCamera: true,
- ),
+ child: inputModel.isPhysicalMouse.value
+ ? getBodyForMobile()
+ : RawTouchGestureDetectorRegion(
+ child: getBodyForMobile(),
+ ffi: gFFI,
+ isCamera: true,
+ ),
);
}),
),
diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart
index 8b645bb88..f6900e5dd 100644
--- a/flutter/lib/mobile/widgets/dialog.dart
+++ b/flutter/lib/mobile/widgets/dialog.dart
@@ -12,6 +12,100 @@ void _showSuccess() {
showToast(translate("Successful"));
}
+void _showError() {
+ showToast(translate("Error"));
+}
+
+void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
+ final pw = await bind.mainGetPermanentPassword();
+ final p0 = TextEditingController(text: pw);
+ final p1 = TextEditingController(text: pw);
+ var validateLength = false;
+ var validateSame = false;
+ dialogManager.show((setState, close, context) {
+ submit() async {
+ close();
+ dialogManager.showLoading(translate("Waiting"));
+ if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
+ dialogManager.dismissAll();
+ _showSuccess();
+ } else {
+ dialogManager.dismissAll();
+ _showError();
+ }
+ }
+
+ return CustomAlertDialog(
+ title: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.password_rounded, color: MyTheme.accent),
+ Text(translate('Set your own password')).paddingOnly(left: 10),
+ ],
+ ),
+ content: Form(
+ autovalidateMode: AutovalidateMode.onUserInteraction,
+ child: Column(mainAxisSize: MainAxisSize.min, children: [
+ TextFormField(
+ autofocus: true,
+ obscureText: true,
+ keyboardType: TextInputType.visiblePassword,
+ decoration: InputDecoration(
+ labelText: translate('Password'),
+ ),
+ controller: p0,
+ validator: (v) {
+ if (v == null) return null;
+ final val = v.trim().length > 5;
+ if (validateLength != val) {
+ // use delay to make setState success
+ Future.delayed(Duration(microseconds: 1),
+ () => setState(() => validateLength = val));
+ }
+ return val
+ ? null
+ : translate('Too short, at least 6 characters.');
+ },
+ ).workaroundFreezeLinuxMint(),
+ TextFormField(
+ obscureText: true,
+ keyboardType: TextInputType.visiblePassword,
+ decoration: InputDecoration(
+ labelText: translate('Confirmation'),
+ ),
+ controller: p1,
+ validator: (v) {
+ if (v == null) return null;
+ final val = p0.text == v;
+ if (validateSame != val) {
+ Future.delayed(Duration(microseconds: 1),
+ () => setState(() => validateSame = val));
+ }
+ return val
+ ? null
+ : translate('The confirmation is not identical.');
+ },
+ ).workaroundFreezeLinuxMint(),
+ ])),
+ onCancel: close,
+ onSubmit: (validateLength && validateSame) ? submit : null,
+ actions: [
+ dialogButton(
+ 'Cancel',
+ icon: Icon(Icons.close_rounded),
+ onPressed: close,
+ isOutline: true,
+ ),
+ dialogButton(
+ 'OK',
+ icon: Icon(Icons.done_rounded),
+ onPressed: (validateLength && validateSame) ? submit : null,
+ ),
+ ],
+ );
+ });
+}
+
void setTemporaryPasswordLengthDialog(
OverlayDialogManager dialogManager) async {
List lengths = ['6', '8', '10'];
diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart
index 001887c0c..81c4dc851 100644
--- a/flutter/lib/models/ab_model.dart
+++ b/flutter/lib/models/ab_model.dart
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
@@ -52,9 +53,7 @@ class AbModel {
RxBool get currentAbLoading => current.abLoading;
bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty;
- final _listPullError = ''.obs;
- RxString get abPullError =>
- _listPullError.value.isNotEmpty ? _listPullError : current.pullError;
+ RxString get currentAbPullError => current.pullError;
RxString get currentAbPushError => current.pushError;
String? _personalAbGuid;
RxBool legacyMode = false.obs;
@@ -69,7 +68,6 @@ class AbModel {
var _syncFromRecentLock = false;
var _timerCounter = 0;
var _cacheLoadOnceFlag = false;
- var _pulledOnce = false;
var listInitialized = false;
var _maxPeerOneAb = 0;
@@ -99,17 +97,10 @@ class AbModel {
print("reset ab model");
addressbooks.clear();
_currentName.value = '';
- _listPullError.value = '';
- _pulledOnce = false;
await bind.mainClearAb();
listInitialized = false;
}
- void clearPullErrors() {
- _listPullError.value = '';
- current.pullError.value = '';
- }
-
// #region ab
/// Pulls the address book data from the server.
///
@@ -119,41 +110,31 @@ class AbModel {
var _pulling = false;
Future pullAb(
{required ForcePullAb? force, required bool quiet}) async {
- if (bind.isDisableAb()) return;
- if (!gFFI.userModel.isLogin) return;
- if (gFFI.userModel.networkError.isNotEmpty) return;
if (_pulling) return;
- if (force == null && _pulledOnce) {
- return;
- }
_pulling = true;
- if (!quiet) {
- _listPullError.value = '';
- current.pullError.value = '';
- }
try {
await _pullAb(force: force, quiet: quiet);
_refreshTab();
} catch (_) {}
_pulling = false;
- _pulledOnce = true;
}
Future _pullAb(
{required ForcePullAb? force, required bool quiet}) async {
+ if (bind.isDisableAb()) return;
+ if (!gFFI.userModel.isLogin) return;
+ if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try {
// Read personal guid every time to avoid upgrading the server without closing the main window
_personalAbGuid = null;
- // `true`: continue init. `false`: stop, error already recorded.
- if (!await _getPersonalAbGuid(quiet: quiet)) {
- return;
- }
+ await _getPersonalAbGuid();
+ // Determine legacy mode based on whether _personalAbGuid is null
legacyMode.value = _personalAbGuid == null;
if (!legacyMode.value && _maxPeerOneAb == 0) {
- await _getAbSettings(quiet: quiet);
+ await _getAbSettings();
}
if (_personalAbGuid != null) {
debugPrint("pull ab list");
@@ -161,7 +142,7 @@ class AbModel {
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
// get all address book name
- await _getSharedAbProfiles(abProfiles, quiet: quiet);
+ await _getSharedAbProfiles(abProfiles);
addressbooks.removeWhere((key, value) =>
abProfiles.firstWhereOrNull((e) => e.name == key) == null);
for (int i = 0; i < abProfiles.length; i++) {
@@ -201,7 +182,6 @@ class AbModel {
}
} catch (e) {
debugPrint("pull ab list error: $e");
- _setListPullError(e, quiet: quiet);
}
} else if (listInitialized &&
(!current.initialized || force == ForcePullAb.current)) {
@@ -217,26 +197,14 @@ class AbModel {
}
}
- void _setListPullError(Object err, {required bool quiet, int? statusCode}) {
- if (!quiet) {
- _listPullError.value =
- '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
- }
- if (statusCode == 401) {
- gFFI.userModel.reset(resetOther: true);
- }
- }
-
- Future _getAbSettings({required bool quiet}) async {
- int? statusCode;
+ Future _getAbSettings() async {
try {
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
- statusCode = resp.statusCode;
- if (statusCode == 404) {
+ if (resp.statusCode == 404) {
debugPrint("HTTP 404, api server doesn't support shared address book");
return false;
}
@@ -245,57 +213,46 @@ class AbModel {
if (json.containsKey('error')) {
throw json['error'];
}
- if (statusCode != 200) {
- throw 'HTTP $statusCode';
+ if (resp.statusCode != 200) {
+ throw 'HTTP ${resp.statusCode}';
}
_maxPeerOneAb = json['max_peer_one_ab'] ?? 0;
return true;
} catch (err) {
debugPrint('get ab settings err: ${err.toString()}');
- _setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}
- /// Loads `/api/ab/personal`.
- /// Returns `true` to continue init, `false` to stop after a real error.
- Future _getPersonalAbGuid({required bool quiet}) async {
- int? statusCode;
+ Future _getPersonalAbGuid() async {
try {
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
- statusCode = resp.statusCode;
- if (statusCode == 404) {
+ if (resp.statusCode == 404) {
debugPrint("HTTP 404, current api server is legacy mode");
- // Old server: keep `_personalAbGuid` null and continue in legacy mode.
- return true;
+ return false;
}
Map json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
- if (statusCode != 200) {
- throw 'HTTP $statusCode';
+ if (resp.statusCode != 200) {
+ throw 'HTTP ${resp.statusCode}';
}
_personalAbGuid = json['guid'];
- // New server: guid is available, continue in non-legacy mode.
return true;
} catch (err) {
debugPrint('get personal ab err: ${err.toString()}');
- _setListPullError(err, quiet: quiet, statusCode: statusCode);
}
- // Real error: stop the current pull.
return false;
}
- Future _getSharedAbProfiles(List profiles,
- {required bool quiet}) async {
+ Future _getSharedAbProfiles(List profiles) async {
final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles";
- int? statusCode;
try {
var uri0 = Uri.parse(api);
final pageSize = 100;
@@ -316,19 +273,13 @@ class AbModel {
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(uri, headers: headers);
- statusCode = resp.statusCode;
- if (statusCode == 404) {
- debugPrint(
- "HTTP 404, api server doesn't support shared address book");
- return false;
- }
Map json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
- if (statusCode != 200) {
- throw 'HTTP $statusCode';
+ if (resp.statusCode != 200) {
+ throw 'HTTP ${resp.statusCode}';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
@@ -351,7 +302,6 @@ class AbModel {
return true;
} catch (err) {
debugPrint('_getSharedAbProfiles err: ${err.toString()}');
- _setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index 7d91b03b3..35001cbf2 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -391,30 +391,14 @@ class FileController {
await Future.delayed(Duration(milliseconds: 100));
- final savedDir = (await bind.sessionGetPeerOption(
+ final dir = (await bind.sessionGetPeerOption(
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
- Future tryOpenReadyDirs() async {
- final dirs = {
- if (directory.value.path.isNotEmpty) directory.value.path,
- if (savedDir.isNotEmpty) savedDir,
- options.value.home,
- };
- for (final dir in dirs) {
- if (await _openDirectoryPath(dir, isBack: true)) {
- return true;
- }
- }
- return false;
- }
-
- var opened = await tryOpenReadyDirs();
+ openDirectory(dir.isEmpty ? options.value.home : dir);
await Future.delayed(Duration(seconds: 1));
- if (!opened) {
- // The peer may become ready during the reconnect delay, so retry the
- // same candidates instead of only retrying the default home directory.
- await tryOpenReadyDirs();
+ if (directory.value.path.isEmpty) {
+ openDirectory(options.value.home);
}
}
@@ -445,23 +429,19 @@ class FileController {
});
}
- Future refresh() async {
- // "." can be both a refresh command and a real remote directory path.
- // Refresh must bypass openDirectory's command dispatch to avoid recursion.
- return await _openDirectoryPath(directory.value.path, isBack: true);
+ Future refresh() async {
+ await openDirectory(directory.value.path);
}
- Future openDirectory(String path, {bool isBack = false}) async {
- if (!isBack && path == ".") {
- return await refresh();
+ Future openDirectory(String path, {bool isBack = false}) async {
+ if (path == ".") {
+ refresh();
+ return;
}
- if (!isBack && path == "..") {
- return await _goToParentDirectory(isBack: isBack);
+ if (path == "..") {
+ goToParentDirectory();
+ return;
}
- return await _openDirectoryPath(path, isBack: isBack);
- }
-
- Future _openDirectoryPath(String path, {bool isBack = false}) async {
if (!isBack) {
pushHistory();
}
@@ -478,10 +458,8 @@ class FileController {
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
fd.format(isWindows, sort: sortBy.value);
directory.value = fd;
- return true;
} catch (e) {
debugPrint("Failed to openDirectory $path: $e");
- return false;
}
}
@@ -509,22 +487,19 @@ class FileController {
goBack();
return;
}
- unawaited(_openDirectoryPath(path, isBack: true).then((_) {}));
+ openDirectory(path, isBack: true);
}
void goToParentDirectory() {
- unawaited(_goToParentDirectory().then((_) {}));
- }
-
- Future _goToParentDirectory({bool isBack = false}) async {
final isWindows = options.value.isWindows;
final dirPath = directory.value.path;
var parent = PathUtil.dirname(dirPath, isWindows);
// specially for C:\, D:\, goto '/'
if (parent == dirPath && isWindows) {
- return await _openDirectoryPath('/', isBack: isBack);
+ openDirectory('/');
+ return;
}
- return await _openDirectoryPath(parent, isBack: isBack);
+ openDirectory(parent);
}
// TODO deprecated this
diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart
index d55cff453..c6ba992d2 100644
--- a/flutter/lib/models/group_model.dart
+++ b/flutter/lib/models/group_model.dart
@@ -343,7 +343,6 @@ class GroupModel {
}
reset() async {
- initialized = false;
groupLoadError.value = '';
deviceGroups.clear();
users.clear();
diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart
index 984d6a25c..675a95e42 100644
--- a/flutter/lib/models/input_model.dart
+++ b/flutter/lib/models/input_model.dart
@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
-import 'package:flutter/foundation.dart';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -16,13 +15,12 @@ import 'package:get/get.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
-import 'input_modifier_utils.dart';
import 'relative_mouse_model.dart';
import '../common.dart';
import '../consts.dart';
/// Mouse button enum.
-enum MouseButtons { left, right, wheel, back, forward }
+enum MouseButtons { left, right, wheel, back }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
@@ -159,8 +157,6 @@ extension ToString on MouseButtons {
return 'wheel';
case MouseButtons.back:
return 'back';
- case MouseButtons.forward:
- return 'forward';
}
}
}
@@ -331,80 +327,6 @@ class ToReleaseKeys {
}
class InputModel {
- // Side mouse button support for Linux.
- // Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
- // natively via GDK and forward through the platform channel.
- static InputModel? _activeSideButtonModel;
- // Tracks per-button which model received a side button down event, so the
- // matching up event is routed there even if the pointer has left the view
- // or a different button was pressed in between.
- static final Map _sideButtonDownModels = {};
- static bool _sideButtonChannelInitialized = false;
-
- /// Each Flutter engine (main window + sub-windows from desktop_multi_window)
- /// runs its own Dart isolate with its own statics. Called from initEnv()
- /// which runs per-engine, so each isolate registers its own handler tied
- /// to its own set of InputModels.
- static void initSideButtonChannel() {
- if (!isLinux) return;
- if (_sideButtonChannelInitialized) return;
- _sideButtonChannelInitialized = true;
-
- const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
- channel.setMethodCallHandler((call) async {
- if (call.method == 'onSideMouseButton') {
- final args = call.arguments as Map;
- final button = args['button'] as String;
- final type = args['type'] as String;
- final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
-
- if (type == 'down') {
- final model = _activeSideButtonModel;
- if (model != null &&
- !(model.isViewOnly && !model.showMyCursor) &&
- model.keyboardPerm &&
- !model.isViewCamera) {
- _sideButtonDownModels[mb] = model;
- // Fire-and-forget to avoid blocking the platform channel handler.
- unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
- debugPrint('[InputModel] failed to send side button $type for $mb: $e');
- }));
- }
- } else {
- // Only route 'up' when we recorded the matching 'down';
- // dropping avoids sending unpaired 'up' to an unrelated session.
- // Use _sendMouseUnchecked to bypass permission checks so the
- // release always goes through even if permissions changed.
- final model = _sideButtonDownModels.remove(mb);
- if (model != null) {
- unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
- debugPrint('[InputModel] failed to send side button $type for $mb: $e');
- }));
- }
- }
- }
- return null;
- });
- }
-
- /// Clear any static references to this model (prevents stale routing).
- /// Releases any held side buttons on the peer so closing a session
- /// mid-press does not leave a stuck button.
- void disposeSideButtonTracking() {
- if (_activeSideButtonModel == this) _activeSideButtonModel = null;
- final held = _sideButtonDownModels.entries
- .where((e) => e.value == this)
- .map((e) => e.key)
- .toList();
- for (final mb in held) {
- _sideButtonDownModels.remove(mb);
- // Best-effort release; session may already be tearing down.
- unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) {
- debugPrint('[InputModel] failed to release side button $mb: $e');
- }));
- }
- }
-
final WeakReference parent;
String keyboardMode = '';
@@ -490,7 +412,6 @@ class InputModel {
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
InputModel(this.parent) {
- initSideButtonChannel();
sessionId = parent.target!.sessionId;
_relativeMouse = RelativeMouseModel(
sessionId: sessionId,
@@ -699,38 +620,6 @@ class InputModel {
}
}
- // Safe: this only re-dispatches synthesized Shift key-up events.
- // The key-up path clears the tracked Shift state so this does not loop.
- void _releaseTrackedShiftKeyEventIfNeeded() {
- final leftShift = toReleaseKeys.lastLShiftKeyEvent;
- final rightShift = toReleaseKeys.lastRShiftKeyEvent;
- if (leftShift != null) {
- handleKeyEvent(leftShift);
- }
- if (rightShift != null) {
- handleKeyEvent(rightShift);
- }
- }
-
- // Safe: this only re-dispatches synthesized Shift key-up events.
- // The raw key-up path clears the tracked Shift state so this does not loop.
- void _releaseTrackedRawShiftKeyEventIfNeeded() {
- final leftShift = toReleaseRawKeys.lastLShiftKeyEvent;
- final rightShift = toReleaseRawKeys.lastRShiftKeyEvent;
- if (leftShift != null) {
- handleRawKeyEvent(RawKeyUpEvent(
- data: leftShift.data,
- character: leftShift.character,
- ));
- }
- if (rightShift != null) {
- handleRawKeyEvent(RawKeyUpEvent(
- data: rightShift.data,
- character: rightShift.character,
- ));
- }
- }
-
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
@@ -785,27 +674,6 @@ class InputModel {
toReleaseRawKeys.updateKeyUp(key, e);
}
- // On some mobile soft-keyboard paths, Flutter may leave cached Shift state
- // set even though the current raw key event is not shifted anymore.
- if (e is RawKeyDownEvent &&
- shouldReleaseStaleMobileShift(
- isMobile: isMobile,
- cachedShiftPressed: shift,
- actualShiftPressed: e.isShiftPressed,
- logicalKey: e.logicalKey,
- hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null ||
- toReleaseRawKeys.lastRShiftKeyEvent != null,
- )) {
- if (kDebugMode) {
- debugPrint(
- 'input: releasing stale mobile Shift before replaying tracked raw '
- 'key-up (logicalKey=${e.logicalKey.keyLabel}, '
- 'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)',
- );
- }
- _releaseTrackedRawShiftKeyEventIfNeeded();
- }
-
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e, iosCapsLock);
@@ -849,8 +717,6 @@ class InputModel {
iosCapsLock = _getIosCapsFromCharacter(e);
}
- // Update cached modifier state before sending the event. The stale mobile
- // Shift release check below relies on this cached state.
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
@@ -888,21 +754,6 @@ class InputModel {
}
}
}
-
- // On some mobile soft-keyboard paths, Flutter may leave cached Shift state
- // set even though the current key event is not shifted anymore.
- if (e is KeyDownEvent &&
- shouldReleaseStaleMobileShift(
- isMobile: isMobile,
- cachedShiftPressed: shift,
- actualShiftPressed: HardwareKeyboard.instance.isShiftPressed,
- logicalKey: e.logicalKey,
- hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null ||
- toReleaseKeys.lastRShiftKeyEvent != null,
- )) {
- _releaseTrackedShiftKeyEventIfNeeded();
- }
-
final isDesktopAndMapMode =
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
if (isMobileAndMapMode || isDesktopAndMapMode) {
@@ -1115,20 +966,13 @@ class InputModel {
return evt;
}
- /// Send mouse event unconditionally (no permission checks).
- /// Used for side button releases that must go through even if permissions
- /// changed after the matching down was sent.
- Future _sendMouseUnchecked(String type, MouseButtons button) async {
- await bind.sessionSendMouse(
- sessionId: sessionId,
- msg: json.encode(modify({'type': type, 'buttons': button.value})));
- }
-
/// Send mouse press event.
Future sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return;
if (isViewCamera) return;
- await _sendMouseUnchecked(type, button);
+ await bind.sessionSendMouse(
+ sessionId: sessionId,
+ msg: json.encode(modify({'type': type, 'buttons': button.value})));
}
void enterOrLeave(bool enter) {
@@ -1138,13 +982,6 @@ class InputModel {
_pointerInsideImage = enter;
_lastWheelTsUs = 0;
- // Track active model for side button events (Linux).
- if (enter) {
- _activeSideButtonModel = this;
- } else if (_activeSideButtonModel == this) {
- _activeSideButtonModel = null;
- }
-
// Fix status
if (!enter) {
resetModifiers();
@@ -1495,16 +1332,6 @@ class InputModel {
return false;
}
- /// iOS may emit a synthesized touch event after a real mouse click.
- /// This helper ignores touch-down events that arrive shortly after a mouse down,
- /// even when the position is far (e.g., near the top edge).
- bool _shouldIgnoreTouchAfterMouse(int nowMs) {
- if (!isIOS) return false;
- const int kTouchAfterMouseWindowMs = 700;
- final dt = nowMs - _lastMouseDownTimeMs;
- return dt >= 0 && dt < kTouchAfterMouseWindowMs;
- }
-
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1517,9 +1344,6 @@ class InputModel {
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
- if (!isPhysicalMouse.value) {
- isPhysicalMouse.value = true;
- }
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
@@ -1529,10 +1353,6 @@ class InputModel {
}
if (e.kind != ui.PointerDeviceKind.mouse) {
- // Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue).
- if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) {
- return;
- }
if (isPhysicalMouse.value) {
isPhysicalMouse.value = false;
}
diff --git a/flutter/lib/models/input_modifier_utils.dart b/flutter/lib/models/input_modifier_utils.dart
deleted file mode 100644
index e65c32790..000000000
--- a/flutter/lib/models/input_modifier_utils.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-import 'package:flutter/services.dart';
-
-/// Returns true when a stale mobile one-shot Shift state should be released
-/// by replaying a tracked Shift key-down as a synthesized key-up.
-///
-/// This is only valid on mobile when Flutter's cached Shift state is still on
-/// (`cachedShiftPressed == true`) but the current hardware/raw event reports
-/// Shift as off (`actualShiftPressed == false`).
-///
-/// A tracked Shift key-down is required so the caller can safely synthesize the
-/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the
-/// Shift key event itself must be processed first; otherwise we could release
-/// the tracked key while still handling the original Shift press/release.
-/// Callers should evaluate this only after their cached modifier state has been
-/// updated for the current event.
-///
-/// When this returns true, the caller logs a line like:
-/// `input: releasing stale mobile Shift before replaying tracked raw key-up`
-/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`.
-bool shouldReleaseStaleMobileShift({
- required bool isMobile,
- required bool cachedShiftPressed,
- required bool actualShiftPressed,
- required LogicalKeyboardKey logicalKey,
- required bool hasTrackedShiftKeyDown,
-}) {
- if (!isMobile || !cachedShiftPressed || actualShiftPressed) {
- return false;
- }
- if (!hasTrackedShiftKeyDown) {
- return false;
- }
- if (logicalKey == LogicalKeyboardKey.shiftLeft ||
- logicalKey == LogicalKeyboardKey.shiftRight) {
- return false;
- }
- return true;
-}
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index e94834a2b..4533f11fa 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -3932,7 +3932,6 @@ class FFI {
inputModel.resetModifiers();
// Dispose relative mouse mode resources to ensure cursor is restored
inputModel.disposeRelativeMouseMode();
- inputModel.disposeSideButtonTracking();
if (closeSession) {
await bind.sessionClose(sessionId: sessionId);
}
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index 40c94fcf5..5892ed0fe 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier {
}
toggleAudio() async {
- if (clients.any((c) => !c.disconnected)) {
+ if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
@@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier {
}
toggleFile() async {
- if (clients.any((c) => !c.disconnected)) {
+ if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_fileOk &&
@@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier {
}
toggleInput() async {
- if (clients.any((c) => !c.disconnected)) {
+ if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (_inputOk) {
@@ -471,6 +471,17 @@ class ServerModel with ChangeNotifier {
WakelockManager.disable(_wakelockKey);
}
+ Future setPermanentPassword(String newPW) async {
+ await bind.mainSetPermanentPassword(password: newPW);
+ await Future.delayed(Duration(milliseconds: 500));
+ final pw = await bind.mainGetPermanentPassword();
+ if (newPW == pw) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
fetchID() async {
final id = await bind.mainGetMyId();
if (id != _serverId.id) {
@@ -549,19 +560,10 @@ class ServerModel with ChangeNotifier {
if (index < 0) {
_clients.add(client);
} else {
- if (_clients[index].authorized) {
- _clients[index].privacyMode = client.privacyMode;
- notifyListeners();
- return;
- }
_clients[index].authorized = true;
- _clients[index].privacyMode = client.privacyMode;
}
} else {
- final index = _clients.indexWhere((c) => c.id == client.id);
- if (index >= 0) {
- _clients[index].privacyMode = client.privacyMode;
- notifyListeners();
+ if (_clients.any((c) => c.id == client.id)) {
return;
}
_clients.add(client);
@@ -827,7 +829,6 @@ class Client {
bool restart = false;
bool recording = false;
bool blockInput = false;
- bool privacyMode = false;
bool disconnected = false;
bool fromSwitch = false;
bool inVoiceCall = false;
@@ -856,7 +857,6 @@ class Client {
restart = json['restart'];
recording = json['recording'];
blockInput = json['block_input'];
- privacyMode = json['privacy_mode'] ?? privacyMode;
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
inVoiceCall = json['in_voice_call'];
@@ -881,7 +881,6 @@ class Client {
data['restart'] = restart;
data['recording'] = recording;
data['block_input'] = blockInput;
- data['privacy_mode'] = privacyMode;
data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch;
data['in_voice_call'] = inVoiceCall;
diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart
index 8961d2dd8..a74241ccb 100644
--- a/flutter/lib/models/terminal_model.dart
+++ b/flutter/lib/models/terminal_model.dart
@@ -27,30 +27,25 @@ class TerminalModel with ChangeNotifier {
// Buffer for output data received before terminal view has valid dimensions.
// This prevents NaN errors when writing to terminal before layout is complete.
final _pendingOutputChunks = [];
- final _pendingOutputSuppressFlags = [];
int _pendingOutputSize = 0;
static const int _kMaxOutputBufferChars = 8 * 1024;
// View ready state: true when terminal has valid dimensions, safe to write
bool _terminalViewReady = false;
- bool _markViewReadyScheduled = false;
- bool _suppressTerminalOutput = false;
- bool _suppressNextTerminalDataOutput = false;
+
+ bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
void Function(int w, int h, int pw, int ph)? onResizeExternal;
Future _handleInput(String data) async {
- // Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
- // real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
- // - Peer Windows: '\r' works, '\n' is just a newline.
- // - Peer Linux: canonical-mode shells accept both, but raw-mode apps
- // (readline, prompt_toolkit, vim, TUI frameworks) expect '\r'.
- // - Peer macOS: same as Linux, raw-mode apps expect '\r'
- // (https://github.com/rustdesk/rustdesk/issues/14907).
- // So on mobile / web-mobile, always normalize a lone '\n' to '\r'.
- // We deliberately do not touch multi-character payloads (e.g. pasted text)
- // so embedded newlines in pasted content are preserved.
+ // If we press the `Enter` button on Android,
+ // `data` can be '\r' or '\n' when using different keyboards.
+ // Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline.
+ // Android -> Linux. Both '\r' and '\n' work as expected (execute a command).
+ // So when we receive '\n', we may need to convert it to '\r' to ensure compatibility.
+ // Desktop -> Desktop works fine.
+ // Check if we are on mobile or web(mobile), and convert '\n' to '\r'.
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
- if (isMobileOrWebMobile && data == '\n') {
+ if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
data = '\r';
}
if (_terminalOpened) {
@@ -75,10 +70,7 @@ class TerminalModel with ChangeNotifier {
terminalController = TerminalController();
// Setup terminal callbacks
- terminal.onOutput = (data) {
- if (_suppressTerminalOutput) return;
- _handleInput(data);
- };
+ terminal.onOutput = _handleInput;
terminal.onResize = (w, h, pw, ph) async {
// Validate all dimensions before using them
@@ -92,7 +84,7 @@ class TerminalModel with ChangeNotifier {
// Mark terminal view as ready and flush any buffered output on first valid resize.
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
if (!_terminalViewReady) {
- _scheduleMarkViewReady();
+ _markViewReady();
}
if (_terminalOpened) {
@@ -118,16 +110,14 @@ class TerminalModel with ChangeNotifier {
void onReady() {
parent.dialogManager.dismissAll();
- // Fire and forget - don't block onReady. If the transport reconnects while
- // this model is still open, re-send OpenTerminal so the remote service marks
- // the persistent session active again and resumes output streaming.
- openTerminal(force: _terminalOpened).catchError((e) {
+ // Fire and forget - don't block onReady
+ openTerminal().catchError((e) {
debugPrint('[TerminalModel] Error opening terminal: $e');
});
}
- Future openTerminal({bool force = false}) async {
- if (_terminalOpened && !force) return;
+ Future openTerminal() async {
+ if (_terminalOpened) return;
// Request the remote side to open a terminal with default shell
// The remote side will decide which shell to use based on its OS
@@ -285,12 +275,9 @@ class TerminalModel with ChangeNotifier {
if (success) {
_terminalOpened = true;
- // On reconnect, the server may replay recent output. That replay can include
- // terminal queries like DSR/DA; xterm answers them through onOutput as
- // "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer.
- final replayTerminalOutput = evt['replay_terminal_output'];
- _suppressNextTerminalDataOutput = replayTerminalOutput == true ||
- message == 'Reconnected to existing terminal with pending output';
+ // On reconnect ("Reconnected to existing terminal"), server may replay recent output.
+ // If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
+ // We intentionally accept this tradeoff for now to keep logic simple.
// Fallback: if terminal view is not yet ready but already has valid
// dimensions (e.g. layout completed before open response arrived),
@@ -298,7 +285,7 @@ class TerminalModel with ChangeNotifier {
if (!_terminalViewReady &&
terminal.viewWidth > 0 &&
terminal.viewHeight > 0) {
- _scheduleMarkViewReady();
+ _markViewReady();
}
// Process any buffered input
@@ -310,16 +297,12 @@ class TerminalModel with ChangeNotifier {
});
final persistentSessions =
- (evt['persistent_sessions'] as List? ?? [])
- .whereType()
- .where((id) => !parent.terminalModels.containsKey(id))
- .toList();
+ evt['persistent_sessions'] as List? ?? [];
if (kWindowId != null && persistentSessions.isNotEmpty) {
DesktopMultiWindow.invokeMethod(
kWindowId!,
kWindowEventRestoreTerminalSessions,
jsonEncode({
- 'peer_id': id,
'persistent_sessions': persistentSessions,
}));
}
@@ -349,8 +332,6 @@ class TerminalModel with ChangeNotifier {
final data = evt['data'];
if (data != null) {
- final suppressTerminalOutput = _suppressNextTerminalDataOutput;
- _suppressNextTerminalDataOutput = false;
try {
String text = '';
if (data is String) {
@@ -370,7 +351,7 @@ class TerminalModel with ChangeNotifier {
return;
}
- _writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
+ _writeToTerminal(text);
} catch (e) {
debugPrint('[TerminalModel] Failed to process terminal data: $e');
}
@@ -380,10 +361,7 @@ class TerminalModel with ChangeNotifier {
/// Write text to terminal, buffering if the view is not yet ready.
/// All terminal output should go through this method to avoid NaN errors
/// from writing before the terminal view has valid layout dimensions.
- void _writeToTerminal(
- String text, {
- bool suppressTerminalOutput = false,
- }) {
+ void _writeToTerminal(String text) {
if (!_terminalViewReady) {
// If a single chunk exceeds the cap, keep only its tail.
// Note: truncation may split a multi-byte ANSI escape sequence,
@@ -395,73 +373,34 @@ class TerminalModel with ChangeNotifier {
_pendingOutputChunks
..clear()
..add(truncated);
- _pendingOutputSuppressFlags
- ..clear()
- ..add(suppressTerminalOutput);
_pendingOutputSize = truncated.length;
} else {
_pendingOutputChunks.add(text);
- _pendingOutputSuppressFlags.add(suppressTerminalOutput);
_pendingOutputSize += text.length;
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
while (_pendingOutputSize > _kMaxOutputBufferChars &&
_pendingOutputChunks.length > 1) {
final removed = _pendingOutputChunks.removeAt(0);
- _pendingOutputSuppressFlags.removeAt(0);
_pendingOutputSize -= removed.length;
}
}
return;
}
- _writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
+ terminal.write(text);
}
void _flushOutputBuffer() {
if (_pendingOutputChunks.isEmpty) return;
debugPrint(
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
- for (var i = 0; i < _pendingOutputChunks.length; i++) {
- _writeTerminalChunk(
- _pendingOutputChunks[i],
- suppressTerminalOutput: _pendingOutputSuppressFlags[i],
- );
+ for (final chunk in _pendingOutputChunks) {
+ terminal.write(chunk);
}
_pendingOutputChunks.clear();
- _pendingOutputSuppressFlags.clear();
_pendingOutputSize = 0;
}
- void _writeTerminalChunk(
- String text, {
- required bool suppressTerminalOutput,
- }) {
- if (!suppressTerminalOutput) {
- terminal.write(text);
- return;
- }
- final previous = _suppressTerminalOutput;
- _suppressTerminalOutput = true;
- try {
- terminal.write(text);
- } finally {
- _suppressTerminalOutput = previous;
- }
- }
-
/// Mark terminal view as ready and flush buffered output.
- void _scheduleMarkViewReady() {
- if (_disposed || _terminalViewReady || _markViewReadyScheduled) return;
- _markViewReadyScheduled = true;
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _markViewReadyScheduled = false;
- if (_disposed || _terminalViewReady) return;
- if (terminal.viewWidth > 0 && terminal.viewHeight > 0) {
- _markViewReady();
- }
- });
- WidgetsBinding.instance.ensureVisualUpdate();
- }
-
void _markViewReady() {
if (_terminalViewReady) return;
_terminalViewReady = true;
@@ -487,10 +426,7 @@ class TerminalModel with ChangeNotifier {
// Clear buffers to free memory
_inputBuffer.clear();
_pendingOutputChunks.clear();
- _pendingOutputSuppressFlags.clear();
_pendingOutputSize = 0;
- _markViewReadyScheduled = false;
- _suppressNextTerminalDataOutput = false;
// Terminal cleanup is handled server-side when service closes
super.dispose();
}
diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart
index 54e6a9a9b..66191d004 100644
--- a/flutter/lib/web/bridge.dart
+++ b/flutter/lib/web/bridge.dart
@@ -1159,6 +1159,10 @@ class RustdeskImpl {
return Future.value('');
}
+ Future mainGetPermanentPassword({dynamic hint}) {
+ return Future.value('');
+ }
+
Future mainGetFingerprint({dynamic hint}) {
return Future.value('');
}
@@ -1342,9 +1346,9 @@ class RustdeskImpl {
throw UnimplementedError("mainUpdateTemporaryPassword");
}
- Future mainSetPermanentPasswordWithResult(
+ Future mainSetPermanentPassword(
{required String password, dynamic hint}) {
- throw UnimplementedError("mainSetPermanentPasswordWithResult");
+ throw UnimplementedError("mainSetPermanentPassword");
}
Future mainCheckSuperUserPermission({dynamic hint}) {
@@ -1538,10 +1542,7 @@ class RustdeskImpl {
Future mainAccountAuth(
{required String op, required bool rememberMe, dynamic hint}) {
- // Safari only allows auth popups while handling the original user gesture.
- // Use Future.sync so the JS call runs synchronously (pre-opening the OIDC
- // window) while any interop error still surfaces as a Future error.
- return Future.sync(() => js.context.callMethod('setByName', [
+ return Future(() => js.context.callMethod('setByName', [
'account_auth',
jsonEncode({'op': op, 'remember': rememberMe})
]));
@@ -1729,7 +1730,7 @@ class RustdeskImpl {
}
String mainSupportedPrivacyModeImpls({dynamic hint}) {
- return '[]';
+ throw UnimplementedError("mainSupportedPrivacyModeImpls");
}
String mainSupportedInputSource({dynamic hint}) {
diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc
index 210adba96..a05bb7856 100644
--- a/flutter/linux/my_application.cc
+++ b/flutter/linux/my_application.cc
@@ -29,80 +29,6 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
extern bool gIsConnectionManager;
-// --- Side mouse button support (back/forward) ---
-// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart.
-// We intercept them via GDK and forward through a dedicated platform channel.
-
-static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons";
-
-static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
- if (event->button != 8 && event->button != 9) {
- return FALSE;
- }
- // Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic
- // events) - only handle real press and release.
- if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) {
- return FALSE;
- }
-
- FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data);
- if (channel == NULL) return FALSE;
-
- g_autoptr(FlValue) args = fl_value_new_map();
- fl_value_set_string_take(args, "button",
- fl_value_new_string(event->button == 8 ? "back" : "forward"));
- fl_value_set_string_take(args, "type",
- fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up"));
-
- fl_method_channel_invoke_method(channel, "onSideMouseButton", args,
- NULL, NULL, NULL);
-
- return TRUE;
-}
-
-static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) {
- g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
- return fl_method_channel_new(
- fl_engine_get_binary_messenger(engine),
- kSideButtonChannelName,
- FL_METHOD_CODEC(codec));
-}
-
-static void side_buttons_channel_destroy(gpointer data) {
- g_object_unref(data);
-}
-
-static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) {
- // Guard against double-initialization (would leave dangling signal user_data).
- if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return;
-
- gtk_widget_add_events(GTK_WIDGET(window),
- GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
- // Store channel on the window so it stays alive and is freed with the window.
- g_object_set_data_full(G_OBJECT(window), "side-buttons-channel",
- g_object_ref(channel), side_buttons_channel_destroy);
- g_signal_connect(window, "button-press-event",
- G_CALLBACK(on_side_button_event), channel);
- g_signal_connect(window, "button-release-event",
- G_CALLBACK(on_side_button_event), channel);
-}
-
-static void on_subwindow_created(FlPluginRegistry* registry) {
-#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
- wayland_shortcuts_inhibit_init_for_subwindow(registry);
-#endif
- // Set up side button forwarding for sub-windows.
- if (registry == NULL || !FL_IS_VIEW(registry)) return;
- FlView* view = FL_VIEW(registry);
- GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view));
- if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
- FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view));
- if (channel == NULL) return;
- side_buttons_init_for_window(GTK_WINDOW(toplevel), channel);
- g_object_unref(channel); // window now owns a ref via g_object_set_data_full
- }
-}
-
GtkWidget *find_gl_area(GtkWidget *widget);
// Implements GApplication::activate.
@@ -170,12 +96,12 @@ static void my_application_activate(GApplication* application) {
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_show(GTK_WIDGET(view));
- // Register callback for sub-windows created by desktop_multi_window plugin.
- // Handles both Wayland shortcuts inhibition (guarded inside) and side button
- // forwarding. Safe to call on X11-only builds - the plugin just stores the
- // callback pointer regardless of windowing system.
+#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
+ // Register callback for sub-windows created by desktop_multi_window plugin
+ // Only sub-windows (remote windows) need keyboard shortcuts inhibition
desktop_multi_window_plugin_set_window_created_callback(
- (WindowCreatedCallback)on_subwindow_created);
+ (WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
+#endif
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
@@ -190,11 +116,6 @@ static void my_application_activate(GApplication* application) {
self,
nullptr);
- // Forward side mouse button events (back/forward) to Dart on the main window.
- FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view));
- side_buttons_init_for_window(window, side_channel);
- g_object_unref(side_channel);
-
gtk_widget_grab_focus(GTK_WIDGET(view));
}
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index eddf5a19d..eb6d76161 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -113,8 +113,8 @@ dependencies:
dev_dependencies:
icons_launcher: ^2.0.4
- flutter_test:
- sdk: flutter
+ #flutter_test:
+ #sdk: flutter
build_runner: ^2.4.6
freezed: ^2.4.2
flutter_lints: ^2.0.2
diff --git a/flutter/test/input_modifier_utils_test.dart b/flutter/test/input_modifier_utils_test.dart
deleted file mode 100644
index 2e1971753..000000000
--- a/flutter/test/input_modifier_utils_test.dart
+++ /dev/null
@@ -1,125 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flutter_hbb/models/input_modifier_utils.dart';
-
-void main() {
- group('shouldReleaseStaleMobileShift', () {
- test('does not release when cached shift is already false', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: false,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.keyD,
- hasTrackedShiftKeyDown: true,
- ),
- isFalse,
- );
- });
-
- test('releases one-shot mobile shift after a text key', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.keyD,
- hasTrackedShiftKeyDown: true,
- ),
- isTrue,
- );
- });
-
- test('does not release manually toggled shift without tracked key down',
- () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.keyD,
- hasTrackedShiftKeyDown: false,
- ),
- isFalse,
- );
- });
-
- test('does not release when shift is still physically pressed', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: true,
- logicalKey: LogicalKeyboardKey.keyD,
- hasTrackedShiftKeyDown: true,
- ),
- isFalse,
- );
- });
-
- test('does not release on non-mobile platforms', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: false,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.keyD,
- hasTrackedShiftKeyDown: true,
- ),
- isFalse,
- );
- });
-
- test('releases on enter key', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.enter,
- hasTrackedShiftKeyDown: true,
- ),
- isTrue,
- );
- });
-
- test('releases on arrow key', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.arrowLeft,
- hasTrackedShiftKeyDown: true,
- ),
- isTrue,
- );
- });
-
- test('does not release on modifier events', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.shiftLeft,
- hasTrackedShiftKeyDown: true,
- ),
- isFalse,
- );
- });
-
- test('does not release on shiftRight modifier events', () {
- expect(
- shouldReleaseStaleMobileShift(
- isMobile: true,
- cachedShiftPressed: true,
- actualShiftPressed: false,
- logicalKey: LogicalKeyboardKey.shiftRight,
- hasTrackedShiftKeyDown: true,
- ),
- isFalse,
- );
- });
- });
-}
diff --git a/libs/clipboard/src/platform/unix/fuse/cs.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs
index fa1dea71d..0f1cf8739 100644
--- a/libs/clipboard/src/platform/unix/fuse/cs.rs
+++ b/libs/clipboard/src/platform/unix/fuse/cs.rs
@@ -12,7 +12,7 @@
//!
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
//! *Need a way to transfer file names with '\' safely*.
-//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes.
+//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes.
//!
//! # Note
//! - all files on FS should be read only, and mark the owner to be the current user
diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c
index 95d1d1a5c..e1856863e 100644
--- a/libs/clipboard/src/windows/wf_cliprdr.c
+++ b/libs/clipboard/src/windows/wf_cliprdr.c
@@ -624,7 +624,6 @@ void CliprdrStream_Delete(CliprdrStream *instance)
if (instance)
{
free(instance->iStream.lpVtbl);
- instance->iStream.lpVtbl = NULL;
free(instance);
}
}
@@ -2161,7 +2160,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
return FALSE;
/* add to name array */
- clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR));
+ clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2);
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs
index 7796904f9..26d090855 100644
--- a/libs/enigo/src/linux/xdo.rs
+++ b/libs/enigo/src/linux/xdo.rs
@@ -8,7 +8,6 @@
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::libc::c_int;
-use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay};
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
use std::{borrow::Cow, ffi::CString};
@@ -33,51 +32,6 @@ fn mousebutton(button: MouseButton) -> c_int {
}
}
-/// Minimum number of buttons the X11 core pointer must support.
-/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons.
-const MIN_POINTER_BUTTONS: usize = 9;
-
-/// Check that the X11 core pointer's button map includes at least 9 buttons
-/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9).
-///
-/// RustDesk's uinput "Mouse passthrough" device normally provides enough
-/// buttons, but we log a warning if the map is too small so the issue is
-/// diagnosable. `XSetPointerMapping` cannot extend the button count (its
-/// length must match `XGetPointerMapping`), so we only diagnose here.
-fn check_x11_button_map() {
- // Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings
- // on pure Wayland or headless environments without $DISPLAY.
- if std::env::var_os("DISPLAY").is_none() {
- return;
- }
-
- let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) };
- if display.is_null() {
- log::warn!("XOpenDisplay failed, cannot check button map");
- return;
- }
-
- let mut current_map = [0u8; 32];
- let nbuttons =
- unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) };
- unsafe { XCloseDisplay(display) };
-
- if nbuttons < 0 {
- log::warn!("XGetPointerMapping failed (returned {nbuttons})");
- return;
- }
-
- let nbuttons = nbuttons as usize;
- if nbuttons >= MIN_POINTER_BUTTONS {
- log::info!("X11 pointer has {nbuttons} buttons, side buttons supported");
- } else {
- log::warn!(
- "X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \
- back/forward side buttons may not work until a device with more buttons is added"
- );
- }
-}
-
/// The main struct for handling the event emitting
pub(super) struct EnigoXdo {
xdo: *mut xdo_t,
@@ -98,7 +52,6 @@ impl Default for EnigoXdo {
log::warn!("Failed to create xdo context, xdo functions will be disabled");
} else {
log::info!("xdo context created successfully");
- check_x11_button_map();
}
Self {
xdo,
diff --git a/libs/hbb_common b/libs/hbb_common
index 9043c15ac..48c37de3e 160000
--- a/libs/hbb_common
+++ b/libs/hbb_common
@@ -1 +1 @@
-Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0
+Subproject commit 48c37de3e6c4e399af6f51ca20e8e3e1fd037976
diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs
index 8ec5e6b8f..bd3eace7b 100644
--- a/libs/scrap/src/common/mediacodec.rs
+++ b/libs/scrap/src/common/mediacodec.rs
@@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option PixelProvider<'a> {
}
pub trait Recorder {
- fn capture(&mut self, timeout_ms: u64) -> Result, Box>;
+ fn capture(&mut self, timeout_ms: u64) -> Result>;
}
pub trait BoxCloneCapturable {
diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs
index 8859d0d3b..d29677c7a 100644
--- a/libs/scrap/src/wayland/pipewire.rs
+++ b/libs/scrap/src/wayland/pipewire.rs
@@ -276,21 +276,12 @@ impl PipeWireRecorder {
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
src.set_property("always-copy", &true)?;
- // COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink.
- // xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream
- // format set is too narrow (appsink only accepts BGRx/RGBx), producing
- // "no more output formats" / not-negotiated (-4). videoconvert accepts any
- // system-memory video/x-raw format, widening negotiation so the portal can
- // settle on a format it can deliver via its SHM path.
- let convert = gst::ElementFactory::make("videoconvert", None)?;
-
let sink = gst::ElementFactory::make("appsink", None)?;
sink.set_property("drop", &true)?;
sink.set_property("max-buffers", &1u32)?;
- pipeline.add_many(&[&src, &convert, &sink])?;
- src.link(&convert)?;
- convert.link(&sink)?;
+ pipeline.add_many(&[&src, &sink])?;
+ src.link(&sink)?;
let appsink = sink
.dynamic_cast::()
@@ -355,7 +346,7 @@ impl PipeWireRecorder {
}
impl Recorder for PipeWireRecorder {
- fn capture(&mut self, timeout_ms: u64) -> Result, Box> {
+ fn capture(&mut self, timeout_ms: u64) -> Result> {
if let Some(sample) = self
.appsink
.try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms))
diff --git a/libs/virtual_display/dylib/README.md b/libs/virtual_display/dylib/README.md
index fb71c3c56..30fa588f1 100644
--- a/libs/virtual_display/dylib/README.md
+++ b/libs/virtual_display/dylib/README.md
@@ -29,4 +29,4 @@ TODO
## X11
-## macOS
+## OSX
diff --git a/res/audits.py b/res/audits.py
old mode 100755
new mode 100644
index 77b0fd120..d843233da
--- a/res/audits.py
+++ b/res/audits.py
@@ -43,7 +43,7 @@ def get_connection_type_name(conn_type):
"""Convert connection type number to readable name"""
type_map = {
0: "Remote Desktop",
- 1: "File Transfer",
+ 1: "File Transfer",
2: "Port Transfer",
3: "View Camera",
4: "Terminal"
@@ -55,7 +55,7 @@ def get_console_type_name(console_type):
"""Convert console audit type number to readable name"""
type_map = {
0: "Group Management",
- 1: "User Management",
+ 1: "User Management",
2: "Device Management",
3: "Address Book Management"
}
@@ -67,7 +67,7 @@ def get_console_operation_name(operation_code):
operation_map = {
0: "User Login",
1: "Add Group",
- 2: "Add User",
+ 2: "Add User",
3: "Add Device",
4: "Delete Groups",
5: "Disconnect Device",
@@ -95,7 +95,7 @@ def get_console_operation_name(operation_code):
def get_alarm_type_name(alarm_type):
"""Convert alarm type number to readable name"""
type_map = {
- 0: "Access attempt outside the IP whitelist",
+ 0: "Access attempt outside the IP whiltelist",
1: "Over 30 consecutive access attempts",
2: "Multiple access attempts within one minute",
3: "Over 30 consecutive login attempts",
@@ -109,24 +109,24 @@ def enhance_audit_data(data, audit_type):
"""Enhance audit data with readable formats"""
if not data:
return data
-
+
enhanced_data = []
for item in data:
enhanced_item = item.copy()
-
+
# Convert timestamps - replace original values
if 'created_at' in enhanced_item:
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
if 'end_time' in enhanced_item:
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
-
+
# Add type-specific enhancements - replace original values
if audit_type == 'conn':
if 'conn_type' in enhanced_item:
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
else:
enhanced_item['conn_type'] = "Not Logged In"
-
+
elif audit_type == 'console':
if 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
@@ -136,14 +136,14 @@ def enhance_audit_data(data, audit_type):
# Replace iop field with operation and convert to readable name
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
del enhanced_item['iop']
-
+
elif audit_type == 'alarm' and 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
del enhanced_item['typ']
-
+
enhanced_data.append(enhanced_item)
-
+
return enhanced_data
@@ -152,7 +152,7 @@ def check_response(response):
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
-
+
try:
response_json = response.json()
if "error" in response_json:
@@ -163,28 +163,28 @@ def check_response(response):
return response.text or "Success"
-def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
+def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
created_at=None, days_ago=None, non_wildcard_fields=None):
"""Common function for viewing audits"""
headers = {"Authorization": f"Bearer {token}"}
-
+
# Set default page size and current page
if page_size is None:
page_size = 10
if current is None:
current = 1
-
+
params = {
"pageSize": page_size,
"current": current
}
-
+
# Add filter parameters if provided
if filters:
for key, value in filters.items():
if value is not None:
params[key] = value
-
+
# Handle time filters
if days_ago is not None:
# Calculate datetime from days ago
@@ -205,10 +205,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
# Apply wildcard patterns for string fields (excluding specific fields)
if non_wildcard_fields is None:
non_wildcard_fields = set()
-
+
# Always exclude these fields from wildcard treatment
non_wildcard_fields.update(["created_at", "pageSize", "current"])
-
+
string_params = {}
for k, v in params.items():
if isinstance(v, str) and k not in non_wildcard_fields:
@@ -221,10 +221,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
response_json = check_response(response)
-
+
# Enhance the data with readable formats
data = enhance_audit_data(response_json.get("data", []), endpoint)
-
+
return {
"data": data,
"total": response_json.get("total", 0),
@@ -233,7 +233,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
}
-def view_conn_audits(url, token, remote=None, conn_type=None,
+def view_conn_audits(url, token, remote=None, conn_type=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View connection audits"""
filters = {
@@ -241,7 +241,7 @@ def view_conn_audits(url, token, remote=None, conn_type=None,
"conn_type": conn_type
}
non_wildcard_fields = {"conn_type"}
-
+
return view_audits_common(
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -254,7 +254,7 @@ def view_file_audits(url, token, remote=None,
"remote": remote
}
non_wildcard_fields = set()
-
+
return view_audits_common(
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -267,7 +267,7 @@ def view_alarm_audits(url, token, device=None,
"device": device
}
non_wildcard_fields = set()
-
+
return view_audits_common(
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -280,7 +280,7 @@ def view_console_audits(url, token, operator=None,
"operator": operator
}
non_wildcard_fields = set()
-
+
return view_audits_common(
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -295,15 +295,15 @@ def main():
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument("--token", required=True, help="Bearer token for authentication")
-
+
# Pagination parameters
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
-
+
# Time filtering parameters
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
-
+
# Audit filters (simplified)
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
@@ -319,9 +319,9 @@ def main():
if args.command == "view-conn":
# View connection audits
result = view_conn_audits(
- args.url,
- args.token,
- args.remote,
+ args.url,
+ args.token,
+ args.remote,
args.conn_type,
args.page_size,
args.current,
@@ -329,12 +329,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
-
+
elif args.command == "view-file":
# View file audits
result = view_file_audits(
- args.url,
- args.token,
+ args.url,
+ args.token,
args.remote,
args.page_size,
args.current,
@@ -342,12 +342,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
-
+
elif args.command == "view-alarm":
# View alarm audits
result = view_alarm_audits(
- args.url,
- args.token,
+ args.url,
+ args.token,
args.device,
args.page_size,
args.current,
@@ -355,12 +355,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
-
+
elif args.command == "view-console":
# View console audits
result = view_console_audits(
- args.url,
- args.token,
+ args.url,
+ args.token,
args.operator,
args.page_size,
args.current,
diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp
index f4780dd87..f21cc7ee1 100644
--- a/res/msi/CustomActions/CustomActions.cpp
+++ b/res/msi/CustomActions/CustomActions.cpp
@@ -31,17 +31,17 @@ LExit:
return WcaFinalize(er);
}
-// Helper function to safely delete a file using handle-based deletion.
-// Directories are refused after opening the handle.
+// Helper function to safely delete a file or directory using handle-based deletion.
+// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
- // Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
+ // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// to prevent following symlinks.
// Use shared access to allow deletion even when other processes have the file open.
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
HANDLE hFile = CreateFileW(
fullPath,
- DELETE | FILE_READ_ATTRIBUTES,
+ DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
@@ -55,21 +55,6 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return FALSE;
}
- BY_HANDLE_FILE_INFORMATION fileInfo;
- if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
- {
- WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
- CloseHandle(hFile);
- return FALSE;
- }
-
- if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
- {
- WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
- CloseHandle(hFile);
- return FALSE;
- }
-
// Use SetFileInformationByHandle to mark for deletion.
// The file will be deleted when the handle is closed.
FILE_DISPOSITION_INFO dispInfo;
@@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}
-BOOL PathEndsWithSlash(LPCWSTR path)
+// Helper function to recursively delete a directory's contents with detailed logging.
+void RecursiveDelete(LPCWSTR path)
{
- size_t length = 0;
- HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
- if (FAILED(hr) || length == 0)
- {
- return FALSE;
- }
-
- WCHAR last = path[length - 1];
- return last == L'\\' || last == L'/';
-}
-
-void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
-{
- if (!(attributes & FILE_ATTRIBUTE_READONLY))
+ // Ensure the path is not empty or null.
+ if (path == NULL || path[0] == L'\0')
{
return;
}
- DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
- if (writableAttributes == 0)
+ // Extra safety: never operate directly on a root path.
+ if (PathIsRootW(path))
{
- writableAttributes = FILE_ATTRIBUTE_NORMAL;
- }
-
- if (SetFileAttributesW(fullPath, writableAttributes))
- {
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
return;
}
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
-}
-
-BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
-{
- WCHAR fullPath[MAX_PATH];
- LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
- HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
- if (FAILED(hr))
- {
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
- return FALSE;
+ // MAX_PATH is enough here since the installer should not be using longer paths.
+ // No need to handle extended-length paths (\\?\) in this context.
+ WCHAR searchPath[MAX_PATH];
+ HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
+ if (FAILED(hr)) {
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
+ return;
}
- DWORD attributes = GetFileAttributesW(fullPath);
- if (attributes == INVALID_FILE_ATTRIBUTES)
+ WIN32_FIND_DATAW findData;
+ HANDLE hFind = FindFirstFileW(searchPath, &findData);
+
+ if (hFind == INVALID_HANDLE_VALUE)
{
- DWORD error = GetLastError();
- if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
+ // This can happen if the directory is empty or doesn't exist, which is not an error in our case.
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
+ return;
+ }
+
+ do
+ {
+ // Skip '.' and '..' directories.
+ if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
- return TRUE;
+ continue;
}
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
- return FALSE;
- }
+ // MAX_PATH is enough here since the installer should not be using longer paths.
+ // No need to handle extended-length paths (\\?\) in this context.
+ WCHAR fullPath[MAX_PATH];
+ hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
+ if (FAILED(hr)) {
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
+ continue;
+ }
- if (attributes & FILE_ATTRIBUTE_DIRECTORY)
+ // Before acting, ensure the read-only attribute is not set.
+ if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
+ {
+ if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
+ {
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
+ }
+ }
+
+ if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+ {
+ // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
+ // Do not follow reparse points, only remove the link itself.
+ if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
+ {
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
+ SafeDeleteItem(fullPath);
+ }
+ else
+ {
+ // Recursively delete directory contents first
+ RecursiveDelete(fullPath);
+ // Then delete the directory itself
+ SafeDeleteItem(fullPath);
+ }
+ }
+ else
+ {
+ // Delete file using safe handle-based deletion
+ SafeDeleteItem(fullPath);
+ }
+ } while (FindNextFileW(hFind, &findData) != 0);
+
+ DWORD lastError = GetLastError();
+ if (lastError != ERROR_NO_MORE_FILES)
{
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
- return FALSE;
+ WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
}
- ClearReadOnlyAttribute(fullPath, attributes);
- WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
- return SafeDeleteItem(fullPath);
+ FindClose(hFind);
}
// See `Package.wxs` for the sequence of this custom action.
@@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
-// ├─ RemoveRuntimeGeneratedFiles - <-- Here
+// ├─ RemoveInstallFolder - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
-UINT __stdcall RemoveRuntimeGeneratedFiles(
+UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
- hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
+ hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &pwzData);
@@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
- ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
+ ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
if (installFolder == NULL || installFolder[0] == L'\0') {
- WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
+ WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
goto LExit;
}
if (PathIsRootW(installFolder)) {
- WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
+ WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
goto LExit;
}
- WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
- DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
+ WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
+
+ RecursiveDelete(installFolder);
+
+ // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
+ // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
LExit:
ReleaseStr(pwzData);
@@ -603,10 +616,10 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
if (IsServiceRunningW(svcName)) {
- WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName);
+ WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stoped after 1000 ms.", svcName);
}
else {
- WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName);
+ WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stoped.", svcName);
}
if (MyDeleteServiceW(svcName)) {
@@ -632,7 +645,7 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
// It's really strange that we need sleep here.
- // But the upgrading may be stuck at "copying new files" because the file is in using.
+ // But the upgrading may be stucked at "copying new files" because the file is in using.
// Steps to reproduce: Install -> stop service in tray --> start service -> upgrade
// Sleep(300);
@@ -745,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall)
}
// Why RegSetValueExW always return 998?
- //
+ //
result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
if (result != ERROR_SUCCESS) {
WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result);
@@ -861,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc
i = 0;
j = 0;
// svcBinary is a string with double quotes, we need to escape it for shell arguments.
- // It is original used for `CreateServiceW`.
+ // It is orignal used for `CreateServiceW`.
// eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service
while (true) {
if (svcBinary[j] == L'"') {
diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def
index d50fbf59b..01b03490c 100644
--- a/res/msi/CustomActions/CustomActions.def
+++ b/res/msi/CustomActions/CustomActions.def
@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
EXPORTS
CustomActionHello
- RemoveRuntimeGeneratedFiles
+ RemoveInstallFolder
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning
diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs
index 6911600e9..de9edb7f3 100644
--- a/res/msi/Package/Components/Folders.wxs
+++ b/res/msi/Package/Components/Folders.wxs
@@ -16,15 +16,8 @@
-
-
-
-
-
-
-
-
-
+
+
diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs
index 952172bdc..337e84ec3 100644
--- a/res/msi/Package/Components/RustDesk.wxs
+++ b/res/msi/Package/Components/RustDesk.wxs
@@ -12,7 +12,7 @@
-
+
@@ -77,21 +77,21 @@
-
-
-
+
+
+
-
+
-
+
-
+
-
+
diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs
index 3a9811eb8..3727c0dd3 100644
--- a/res/msi/Package/Fragments/CustomActions.wxs
+++ b/res/msi/Package/Fragments/CustomActions.wxs
@@ -5,7 +5,7 @@
-
+
diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs
index 06c37097c..bf59d569c 100644
--- a/res/msi/Package/UI/MyInstallDlg.wxs
+++ b/res/msi/Package/UI/MyInstallDlg.wxs
@@ -23,13 +23,12 @@ Patch dialog sequence:
-->
-
-
+
@@ -65,16 +64,9 @@ Patch dialog sequence:
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/cli.rs b/src/cli.rs
index 2f3b3550f..f61bfe92f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -25,13 +25,7 @@ impl Session {
pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self {
let mut password = "".to_owned();
if PeerConfig::load(id).password.is_empty() {
- match rpassword::prompt_password("Enter password: ") {
- Ok(p) => password = p,
- Err(e) => {
- log::error!("Failed to read password: {:?}", e);
- password = "".to_owned();
- }
- }
+ password = rpassword::prompt_password("Enter password: ").unwrap();
}
let session = Self {
id: id.to_owned(),
diff --git a/src/client.rs b/src/client.rs
index 321a49ee6..8ea70898f 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -119,13 +119,10 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
#[cfg(target_os = "linux")]
-pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required";
+pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version.";
#[cfg(target_os = "linux")]
pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
- "wayland-requires-higher-linux-version";
-#[cfg(target_os = "linux")]
-pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str =
- "xdp-portal-unavailable";
+ "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.";
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
@@ -1745,9 +1742,6 @@ pub struct LoginConfigHandler {
pub direct: Option,
pub received: bool,
switch_uuid: Option,
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- switch_back_allowed: bool,
pub save_ab_password_to_recent: bool, // true: connected with ab password
pub other_server: Option<(String, String, String)>,
pub custom_fps: Arc>>,
@@ -1864,11 +1858,6 @@ impl LoginConfigHandler {
self.direct = None;
self.received = false;
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- {
- self.switch_back_allowed = false;
- }
self.switch_uuid = switch_uuid;
self.adapter_luid = adapter_luid;
self.selected_windows_session_id = None;
@@ -1882,23 +1871,6 @@ impl LoginConfigHandler {
self.is_terminal_admin = is_terminal_admin;
}
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- pub fn allow_switch_back_once(&mut self) {
- self.switch_back_allowed = true;
- }
-
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- pub fn consume_switch_back_permission(&mut self) -> bool {
- if self.switch_back_allowed {
- self.switch_back_allowed = false;
- true
- } else {
- false
- }
- }
-
/// Check if the client should auto login.
/// Return password if the client should auto login, otherwise return empty string.
pub fn should_auto_login(&self) -> String {
@@ -3402,36 +3374,6 @@ pub fn handle_login_error(
}
}
-#[cfg(feature = "flutter")]
-#[cfg(not(any(target_os = "android", target_os = "ios")))]
-async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
- let Ok(mut conn) = crate::ipc::connect(1000, "").await else {
- return false;
- };
- let uuid = uuid.to_string();
- if conn
- .send(&crate::ipc::Data::SwitchSidesUuid(
- uuid.clone(),
- id.to_owned(),
- None,
- ))
- .await
- .is_err()
- {
- return false;
- }
- match conn.next_timeout(1000).await {
- Ok(Some(crate::ipc::Data::SwitchSidesUuid(
- returned_uuid,
- returned_id,
- Some(true),
- ))) => {
- returned_uuid == uuid && returned_id == id
- }
- _ => false,
- }
-}
-
/// Handle hash message sent by peer.
/// Hash will be used for login.
///
@@ -3452,22 +3394,12 @@ pub async fn handle_hash(
// Take care of password application order
// switch_uuid
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- {
- let uuid = lc.write().unwrap().switch_uuid.take();
- if let Some(uuid) = uuid {
- if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
- let id = lc.read().unwrap().id.clone();
- if !consume_local_switch_sides_uuid(&id, &uuid).await {
- log::warn!("Ignored untrusted switch_uuid");
- } else {
- lc.write().unwrap().allow_switch_back_once();
- send_switch_login_request(lc.clone(), peer, uuid).await;
- lc.write().unwrap().password_source = Default::default();
- return;
- }
- }
+ let uuid = lc.write().unwrap().switch_uuid.take();
+ if let Some(uuid) = uuid {
+ if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
+ send_switch_login_request(lc.clone(), peer, uuid).await;
+ lc.write().unwrap().password_source = Default::default();
+ return;
}
}
// last password
@@ -3935,7 +3867,6 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
&& !text.to_lowercase().contains("resolve")
&& !text.to_lowercase().contains("mismatch")
&& !text.to_lowercase().contains("manually")
- && !text.to_lowercase().contains("restricted")
&& !text.to_lowercase().contains("not allowed")))
}
diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs
index 5eb7a273a..e0b3fcd6d 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -586,6 +586,7 @@ impl Remote {
file_num,
include_hidden,
is_remote,
+ Vec::new(),
od,
));
allow_err!(
@@ -658,6 +659,7 @@ impl Remote {
file_num,
include_hidden,
is_remote,
+ Vec::new(),
od,
);
job.is_last_job = true;
@@ -843,7 +845,19 @@ impl Remote {
}
}
Data::CancelJob(id) => {
- self.cancel_transfer_job(id, peer).await;
+ let mut msg_out = Message::new();
+ let mut file_action = FileAction::new();
+ file_action.set_cancel(FileTransferCancel {
+ id: id,
+ ..Default::default()
+ });
+ msg_out.set_file_action(file_action);
+ allow_err!(peer.send(&msg_out).await);
+ if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
+ job.remove_download_file();
+ }
+ let _ = fs::remove_job(id, &mut self.read_jobs);
+ self.remove_jobs.remove(&id);
}
Data::RemoveDir((id, path)) => {
let mut msg_out = Message::new();
@@ -1039,22 +1053,6 @@ impl Remote {
}
}
- async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) {
- let mut msg_out = Message::new();
- let mut file_action = FileAction::new();
- file_action.set_cancel(FileTransferCancel {
- id,
- ..Default::default()
- });
- msg_out.set_file_action(file_action);
- allow_err!(peer.send(&msg_out).await);
- if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
- job.remove_download_file();
- }
- let _ = fs::remove_job(id, &mut self.read_jobs);
- self.remove_jobs.remove(&id);
- }
-
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
if !self.is_connected {
return false;
@@ -1448,23 +1446,6 @@ impl Remote {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
- #[cfg(target_os = "ios")]
- {
- if let Some(cb) = _mcb
- .clipboards
- .iter()
- .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text))
- {
- let content = if cb.compress {
- hbb_common::compress::decompress(&cb.content)
- } else {
- cb.content.to_vec()
- };
- if let Ok(content) = String::from_utf8(content) {
- self.handler.clipboard(content);
- }
- }
- }
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
}
@@ -1489,43 +1470,14 @@ impl Remote {
fs::transform_windows_path(&mut entries);
}
}
- // We cannot call cancel_transfer_job/handle_job_status while holding
- // a mutable borrow from fs::get_job(&mut self.write_jobs), so defer
- // the error handling until after the borrow scope ends.
- let mut set_files_err = None;
+ self.handler
+ .update_folder_files(fd.id, &entries, fd.path, false, false);
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
log::info!("job set_files: {:?}", entries);
- if let Err(err) = job.set_files(entries) {
- set_files_err = Some(err.to_string());
- } else {
- job.set_finished_size_on_resume();
- self.handler.update_folder_files(
- fd.id,
- job.files(),
- fd.path,
- false,
- false,
- );
- }
+ job.set_files(entries);
+ job.set_finished_size_on_resume();
} else if let Some(job) = self.remove_jobs.get_mut(&fd.id) {
- // Intentionally keep raw entries here:
- // - remote remove flow executes deletions on peer side;
- // - local remove flow is populated from local get_recursive_files().
job.files = entries;
- self.handler
- .update_folder_files(fd.id, &job.files, fd.path, false, false);
- } else {
- self.handler
- .update_folder_files(fd.id, &entries, fd.path, false, false);
- }
- if let Some(err) = set_files_err {
- log::warn!(
- "Rejected unsafe file list from remote peer for job {}: {}",
- fd.id,
- err
- );
- self.cancel_transfer_job(fd.id, peer).await;
- self.handle_job_status(fd.id, -1, Some(err));
}
}
Some(file_response::Union::Digest(digest)) => {
@@ -1797,9 +1749,6 @@ impl Remote {
Ok(Permission::BlockInput) => {
self.handler.set_permission("block_input", p.enabled);
}
- Ok(Permission::PrivacyMode) => {
- self.handler.set_permission("privacy_mode", p.enabled);
- }
_ => {}
}
}
@@ -1923,23 +1872,9 @@ impl Remote {
);
}
}
- #[cfg(feature = "flutter")]
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::SwitchBack(_)) => {
- let allow_switch_back = self
- .handler
- .lc
- .write()
- .unwrap()
- .consume_switch_back_permission();
- if allow_switch_back {
- self.handler.switch_back(&self.handler.get_id());
- } else {
- log::warn!(
- "Ignored unsolicited SwitchBack from {}",
- self.handler.get_id()
- );
- }
+ #[cfg(feature = "flutter")]
+ self.handler.switch_back(&self.handler.get_id());
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
diff --git a/src/common.rs b/src/common.rs
index 69e3ec304..3e23770c6 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -39,7 +39,7 @@ use hbb_common::{
use crate::{
hbbs_http::{create_http_client_async, get_url_for_tls},
- ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
+ ui_interface::{get_option, is_installed, set_option},
};
#[derive(Debug, Eq, PartialEq)]
@@ -1086,7 +1086,6 @@ fn get_api_server_(api: String, custom: String) -> String {
#[inline]
pub fn is_public(url: &str) -> bool {
- let url = url.to_ascii_lowercase();
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
}
@@ -1124,286 +1123,22 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
format!("{}/api/audit/{}", url, typ)
}
-/// Check if we should use raw TCP proxy for API calls.
-/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
-/// and the target URL belongs to the configured non-public API host.
-#[inline]
-fn should_use_raw_tcp_for_api(url: &str) -> bool {
- get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
- && !use_ws()
- && is_tcp_proxy_api_target(url)
-}
-
-/// Check if we can attempt raw TCP proxy fallback for this target URL.
-#[inline]
-fn can_fallback_to_raw_tcp(url: &str) -> bool {
- !use_ws() && is_tcp_proxy_api_target(url)
-}
-
-#[inline]
-fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
- if api_url.is_empty() || is_public(api_url) {
- return false;
- }
-
- let target_host = url::Url::parse(url)
- .ok()
- .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
- let api_host = url::Url::parse(api_url)
- .ok()
- .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
-
- matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
-}
-
-#[inline]
-fn is_tcp_proxy_api_target(url: &str) -> bool {
- should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
-}
-
-fn tcp_proxy_log_target(url: &str) -> String {
- url::Url::parse(url)
- .ok()
- .map(|parsed| {
- let mut redacted = format!("{}://", parsed.scheme());
- let Some(host) = parsed.host() else {
- return "".to_owned();
- };
- redacted.push_str(&host.to_string());
- if let Some(port) = parsed.port() {
- redacted.push(':');
- redacted.push_str(&port.to_string());
- }
- redacted.push_str(parsed.path());
- redacted
- })
- .unwrap_or_else(|| "".to_owned())
-}
-
-#[inline]
-fn get_tcp_proxy_addr() -> String {
- check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
-}
-
-/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
-/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
-/// receives `HttpProxyResponse`.
-///
-/// The entire operation (connect + handshake + send + receive) is wrapped in
-/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
-/// any stage cannot block the caller indefinitely.
-async fn tcp_proxy_request(
- method: &str,
- url: &str,
- body: &[u8],
- headers: Vec,
-) -> ResultType {
- let tcp_addr = get_tcp_proxy_addr();
- if tcp_addr.is_empty() {
- bail!("No rendezvous server configured for TCP proxy");
- }
-
- let parsed = url::Url::parse(url)?;
- let path = if let Some(query) = parsed.query() {
- format!("{}?{}", parsed.path(), query)
- } else {
- parsed.path().to_string()
- };
-
- log::debug!(
- "Sending {} {} via TCP proxy to {}",
- method,
- parsed.path(),
- tcp_addr
- );
-
- let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
- timeout(overall_timeout, async {
- let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
- let key = crate::get_key(true).await;
- secure_tcp_silent(&mut conn, &key).await?;
-
- let mut req = HttpProxyRequest::new();
- req.method = method.to_uppercase();
- req.path = path;
- req.headers = headers.into();
- req.body = Bytes::from(body.to_vec());
-
- let mut msg_out = RendezvousMessage::new();
- msg_out.set_http_proxy_request(req);
- conn.send(&msg_out).await?;
-
- match conn.next().await {
- Some(Ok(bytes)) => {
- let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
- match msg_in.union {
- Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
- _ => bail!("Unexpected response from TCP proxy"),
- }
- }
- Some(Err(e)) => bail!("TCP proxy read error: {}", e),
- None => bail!("TCP proxy connection closed without response"),
- }
- })
- .await?
-}
-
-/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
-/// If the caller supplies a Content-Type header it overrides the default `application/json`.
-fn parse_simple_header(header: &str) -> Vec {
- let mut entries = Vec::new();
- let mut has_content_type = false;
- if !header.is_empty() {
- let tmp: Vec<&str> = header.splitn(2, ": ").collect();
- if tmp.len() == 2 {
- if tmp[0].eq_ignore_ascii_case("Content-Type") {
- has_content_type = true;
- }
- entries.push(HeaderEntry {
- name: tmp[0].into(),
- value: tmp[1].into(),
- ..Default::default()
- });
- }
- }
- if !has_content_type {
- entries.insert(
- 0,
- HeaderEntry {
- name: "Content-Type".into(),
- value: "application/json".into(),
- ..Default::default()
- },
- );
- }
- entries
-}
-
-/// POST request via TCP proxy.
-async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType {
- let headers = parse_simple_header(header);
- let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
- if !resp.error.is_empty() {
- bail!("TCP proxy error: {}", resp.error);
- }
- Ok(String::from_utf8_lossy(&resp.body).to_string())
-}
-
-fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType {
- if !resp.error.is_empty() {
- bail!("TCP proxy error: {}", resp.error);
- }
-
- let mut response_headers = Map::new();
- for entry in resp.headers.iter() {
- response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
- }
-
- let mut result = Map::new();
- result.insert("status_code".to_string(), json!(resp.status));
- result.insert("headers".to_string(), Value::Object(response_headers));
- result.insert(
- "body".to_string(),
- json!(String::from_utf8_lossy(&resp.body)),
- );
-
- serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
-}
-
-fn parse_json_header_entries(header: &str) -> ResultType> {
- let v: Value = serde_json::from_str(header)?;
- if let Value::Object(obj) = v {
- Ok(obj
- .iter()
- .map(|(key, value)| HeaderEntry {
- name: key.clone(),
- value: value.as_str().unwrap_or_default().into(),
- ..Default::default()
- })
- .collect())
- } else {
- Err(anyhow!("HTTP header information parsing failed!"))
- }
-}
-
-/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
-async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
+pub async fn post_request(url: String, body: String, header: &str) -> ResultType {
let proxy_conf = Config::get_socks();
- let tls_url = get_url_for_tls(url, &proxy_conf);
+ let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = post_request_(
- url,
+ &url,
tls_url,
- body.to_owned(),
+ body.clone(),
header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await?;
- let status = response.status().as_u16();
- let text = response.text().await?;
- Ok((status, text))
-}
-
-/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
-/// if the URL is eligible. 4xx responses are returned as-is.
-async fn with_tcp_proxy_fallback(
- url: &str,
- method: &str,
- http_fn: HttpFut,
- tcp_fn: TcpFut,
-) -> ResultType
-where
- HttpFut: Future