Compare commits

..

6 Commits

Author SHA1 Message Date
rustdesk
24a16d9a30 fix: address codex review findings on probe/watchdog fallback
- a probe or raster-stall failure record now also downgrades sessions
  that are already running: main_set_local_option broadcasts the
  fallback for failed-* health writes, which also closes the hole where
  the watchdog's idempotence guard no-oped after the probe had already
  written the record
- probe success requires a frame timing newer than the first push:
  'consumed' advances inside the plugin callback before the GL/Metal
  upload, so a raster thread hanging in the driver no longer counts as
  a pass (and cannot clear a previous failure)
- watchdog failures are tagged with the failing backend
  (failed-watchdog-rgba/gpu); the rgba-only probe clears only the rgba
  class, so a working pixel-buffer path can no longer re-enable a
  broken D3D shared-handle path every launch
- the watchdog pauses while the session's window is hidden (new
  session_set_render_visible FFI wired to the window minimize/restore
  events): a display registered in a minimized window no longer records
  a false global failure; observation now counts pushes within the
  window rather than since registration
- probe failure verdicts additionally require a resumed lifecycle,
  matching the raster-stall monitor
- linux plugin: deferred-unref grace lengthened to 10s (no raster-side
  completion barrier exists; documented as heuristic), ref bumped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:30:15 +08:00
rustdesk
8fc82d04ac keep lockfile at repo resolution, only bump the two plugin refs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:48:28 +08:00
rustdesk
c6c001a15e bump texture plugins with adversarial-review fixes
flutter_texture_rgba_renderer 7932bf9: linux double-free/terminate-UAF
fixes, exported GetConsumed (was invisible to dlsym under hidden
visibility - the watchdog was silently disabled on Linux), C++14
shared_timed_mutex; macos autoreleasepool + failed-registration guard.
flutter_gpu_texture_renderer 208619e: rendering_ no longer sticks true
before the first populate; honest GetConsumed semantics (descriptor
fetches; EGL bind failure still advances it) - noted at the gpu
watchdog call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:47:54 +08:00
rustdesk
7da2bbe6ac fix: address texture watchdog/probe review findings
- watchdog: compare the plugin's cumulative consumed counter against a
  snapshot taken when arming (re-arm was a no-op before), and judge on
  cumulative pushes + elapsed time so sparse damage-driven streams are
  still detected
- failure handling is idempotent (one record per breakage) and updates
  every render session like main_set_local_option does, not only the
  failing one; the fallback toast is not claimed for multi-display
  windows the soft path cannot rescue
- probe: skip when the consumed API is missing (old plugin) or a
  raster-stall is recorded (compositing could hang the main window);
  a fail verdict requires fresh frame timings and a non-minimized
  window; a pass never clears failed-raster-stall; toast only when
  texture rendering was effectively on; probe pixel is transparent
- raster-stall monitor: judge on frame-timing staleness (a mid-episode
  hang was undetectable before), gate on lifecycle/minimized/idle with
  a moving quiet anchor, threshold 30s, shared with the camera page;
  clear stateGlobal minimized flag on plain window restore
- adopt mismatched frame sizes only in multi-ui-session mode (legacy
  mode pairs frames loosely and could ping-pong between displays)
- drop the now-dead closeSession parameter from texture destroy();
  trim comments to repo style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:22:13 +08:00
rustdesk
c5adac828b bump hbb_common to merged main (texture-render-health key)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:58:31 +08:00
rustdesk
7c23e1f4b9 fix: texture render lifetime protocol, watchdog fallback, startup probe
Root cause of #15848 (and the long-standing macOS #6296 / Linux #3343
class): raw native texture pointers are shared across the platform
thread, the engine raster thread and the video thread, with teardown
ordered by a 100 ms sleep - or, when moving a tab to a new window, by
nothing at all. A lost race frees the texture while it is still in use:
the raster thread parks on a destroyed lock (frozen/black view, a
never-presented 'transparent' hole, every later session black) and the
video thread hangs while holding session locks (app half-dead until
restart, still reported as Responding).

- unregister textures with compare-and-clear (new session_unregister_*
  FFI): a late clear can no longer wipe a new window's registration
  (#8016) and Rust never keeps pushing into a freed texture; the 100 ms
  sleeps are gone (the plugins now drain in-flight pushes and defer
  object deletion until the raster thread is done)
- guard the async texture create path against destroy racing it (#13596)
- per-display locks: the per-frame plugin call no longer holds
  session-level locks, so a stalled plugin or driver call cannot freeze
  every window's UI thread
- adopt the frame size after 30 consecutive mismatches instead of
  dropping frames forever (silent black screen on a live connection)
- watchdog: frames pushed but never consumed by the engine fall the
  session back to software rendering live, record texture-render-health,
  and flip the effective default off; toggling the option clears the
  record and re-arms validation
- Dart raster-stall monitor records a hung raster thread for the next
  launch (rendering cannot be rescued in-process in that state)
- startup probe: render one frame through a 1x1 texture in the main
  window each launch; failure disables texture rendering before the
  first session goes black, a pass self-heals a stale failure record

Platform defaults are unchanged (macOS off, Win10+ on, Linux on).
Requires flutter_texture_rgba_renderer ad4c37e and
flutter_gpu_texture_renderer 767bb9f (pinned in pubspec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:54:17 +08:00
187 changed files with 9833 additions and 3152 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
target-branch: "master"
schedule:
interval: "daily"
commit-message:
prefix: "Git submodule"
labels:
- "dependencies"

View File

@@ -124,6 +124,7 @@ jobs:
gcc \
git \
g++ \
libpam0g-dev \
libasound2-dev \
libunwind-dev \
libgstreamer1.0-dev \

View File

@@ -1009,6 +1009,7 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1282,6 +1283,7 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1572,6 +1574,7 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1893,6 +1896,7 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -2122,6 +2126,7 @@ jobs:
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
liblzma-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -2198,7 +2203,7 @@ jobs:
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'sparse+https://index.crates.io/'
registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config
cat ~/.cargo/config
# install dependencies from vcpkg

View File

@@ -271,6 +271,7 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \

View File

@@ -63,24 +63,17 @@
### Comments
* Avoid comments unless they explain a non-obvious reason, constraint, or workaround.
* Never restate what the code does; prefer clearer code instead.
* If the code is self-explanatory, add no comment.
* Keep them short: one line by default, three at most.
* Say **why**, never what. If the code already says it, delete the comment.
* A comment must never be longer than the code it describes.
* Applies to YAML, shell and Python too, not just Rust.
### Be minimally invasive
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
* Do not extract or reshape existing code just to enable your new code; look for a mechanism that leaves existing lines untouched (e.g. hide/show an existing object instead of refactoring its construction into a helper for rebuilding).
* Accept a little duplication over a restructure. A new function that repeats a few lines of an existing one is a better diff than reshaping the original so both can share it.
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
## Reviewing a PR
* Review only what the diff introduces. Verify ownership with `gh pr diff` before reporting a finding — if the offending lines are untouched context, it is a pre-existing problem, not this PR's.
* List pre-existing problems in a separate section at the end, or leave out the ones that are not fatal. Never mix them into the findings the author has to fix.
* Before re-reviewing, read the author's reply comments. Do not re-raise items they declined on scope grounds.
* State a finding's consequence exactly: distinguish "the value is lost" from "the shortcut is inert but the value still saves".
## Localization (`src/lang/*.rs`)
Each file is a `HashMap<key, translation>`. Layout:

View File

@@ -1 +1 @@
@AGENTS.md
AGENTS.md

144
Cargo.lock generated
View File

@@ -986,6 +986,27 @@ dependencies = [
"serde 1.0.228",
]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cacao"
version = "0.4.0-beta2"
@@ -1456,8 +1477,8 @@ dependencies = [
"compression-core",
"flate2",
"memchr",
"zstd",
"zstd-safe",
"zstd 0.13.1",
"zstd-safe 7.1.0",
]
[[package]]
@@ -1528,6 +1549,12 @@ dependencies = [
"unicode-xid 0.2.4",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.2.6"
@@ -3791,14 +3818,14 @@ dependencies = [
"toml 0.7.8",
"tungstenite",
"url",
"users",
"users 0.11.0",
"uuid",
"webpki-roots 1.0.9",
"webrtc",
"whoami",
"winapi 0.3.9",
"x11 2.21.0",
"zstd",
"zstd 0.13.1",
]
[[package]]
@@ -5932,6 +5959,37 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "pam"
version = "0.7.0"
source = "git+https://github.com/rustdesk-org/pam#7bfd25510202cd269292cbdd7c71f3977a6fd762"
dependencies = [
"libc",
"pam-macros",
"pam-sys",
"users 0.10.0",
]
[[package]]
name = "pam-macros"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.36",
"syn 1.0.109",
]
[[package]]
name = "pam-sys"
version = "1.0.0-alpha4"
source = "git+https://github.com/rustdesk-org/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
dependencies = [
"bindgen 0.59.2",
"libc",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -5999,12 +6057,35 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@@ -7235,6 +7316,7 @@ dependencies = [
"once_cell",
"openssl",
"os-version",
"pam",
"parity-tokio-ipc",
"percent-encoding",
"piet",
@@ -7283,6 +7365,7 @@ dependencies = [
"wol-rs",
"x11-clipboard 0.8.1",
"x11rb 0.12.0",
"zip",
]
[[package]]
@@ -8824,7 +8907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
dependencies = [
"base32",
"constant_time_eq",
"constant_time_eq 0.2.6",
"hmac",
"rand 0.8.5",
"sha1",
@@ -9269,6 +9352,16 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "users"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
dependencies = [
"libc",
"log",
]
[[package]]
name = "users"
version = "0.11.0"
@@ -11064,13 +11157,52 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq 0.1.5",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time 0.3.36",
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe 5.0.2+zstd.1.5.2",
]
[[package]]
name = "zstd"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
dependencies = [
"zstd-safe",
"zstd-safe 7.1.0",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]

View File

@@ -37,6 +37,7 @@ drm = ["scrap/drm"]
# kind of operation and deserves a switch that can remove it from the binary entirely, without
# giving up DRM capture: `--features drm` builds the capture path with no wake code compiled in.
drm-wake = ["drm"]
plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
unix-file-copy-paste = [
"dep:x11-clipboard",
@@ -80,6 +81,7 @@ hex = "0.4"
chrono = "0.4"
cidr-utils = "0.5"
fon = "0.6"
zip = "0.6"
shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
stunclient = "0.4"
@@ -189,6 +191,7 @@ async-process = "1.7"
evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
pam = { git="https://github.com/rustdesk-org/pam" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true}
@@ -209,7 +212,7 @@ android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
exclude = ["vdi/host"]
exclude = ["vdi/host", "examples/custom_plugin"]
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)

View File

@@ -19,6 +19,7 @@ RUN apt update -y && \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libasound2-dev \
libpam0g-dev \
libpulse-dev \
make \
wget \

View File

@@ -66,19 +66,19 @@ Please download Sciter dynamic library yourself.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -168,6 +168,7 @@ Please ensure that you run these commands from the root of the RustDesk reposito
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client
## Screenshots

View File

@@ -58,6 +58,7 @@ AppDir:
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
@@ -76,13 +77,6 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
# resolves against the CWD, and that must not outrank the defaults below.
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0

View File

@@ -61,6 +61,7 @@ AppDir:
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
@@ -79,13 +80,6 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
# resolves against the CWD, and that must not outrank the defaults below.
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0

View File

@@ -364,7 +364,7 @@ Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, gstreamer1.0-pipewire%s
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.
@@ -390,9 +390,9 @@ def ffi_bindgen_function_refactor():
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
# any workflow, so a bump is one edit here (plus the informational version comment in
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.4.
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.2.
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
LIBDRMTAP_SHA_PINNED = '5da68a3a368db569716d0d0f11cefacbb11b2290'
LIBDRMTAP_SHA_PINNED = '653de8c774bc245eaf960611ca7a136f7a544d21'
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
@@ -704,6 +704,8 @@ def build_flutter_deb(version, features):
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/etc/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
@@ -722,6 +724,12 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2(
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
system2(
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
@@ -1124,7 +1132,13 @@ def main():
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
os.system('mkdir -p tmpdeb/etc/rustdesk/')
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')

View File

@@ -24,7 +24,7 @@ Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://
## Tindakan
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md>
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
## Komunikasi

View File

@@ -30,7 +30,7 @@ Per istruzioni specifiche su git, vedi [Workflow GitHub - 101](https://github.co
## Condotta
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-IT.md
## Comunicazioni

View File

@@ -160,6 +160,7 @@ RustDesk يرجى التأكد من أنك تنفذ هذه الأوامر من
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**:Flutter لعميل الويب الخاص ب Javascript
## لقطات

View File

@@ -144,6 +144,7 @@ Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk,
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), očekávání vzdálených příméhých („proděrováváním“ TCP) nebo předávaných (relay) spojení
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikačním rámcem (framework) Flutter pro mobilní platformy
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript pro Flutter webový klient
## Ukázky

View File

@@ -66,19 +66,19 @@ Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -168,6 +168,7 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
## Screenshots

View File

@@ -62,19 +62,19 @@ Por favor descarga la librería dinámica de Sciter tú mismo.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -163,6 +163,7 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed")
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
> [!Precaución]
> **Descargo de responsabilidad por uso indebido:** <br>

View File

@@ -146,6 +146,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
## تصاویر محیط نرم‌افزار

View File

@@ -158,6 +158,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Στιγμιότυπα

View File

@@ -48,7 +48,7 @@ A telefonos verziók Flutter-t hasznának. Később lehetséges hogy Sciterről
- Futtasd a `cargo run` parancsot
## [Építés](https://rustdesk.com/docs/en/dev/build/)
## [Építés](https://rustdesk.com/docs/hu/dev/build/)
## Hogyan építs Linuxon
@@ -150,6 +150,7 @@ Kérlek mindenképpen nézd meg hogy ezeket a parancsokat a root RustDesk mappá
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
## Képernyőképek

View File

@@ -162,6 +162,7 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
> [!Attenzione]
> **Dichiarazione di non responsabilità per uso improprio:** <br>

View File

@@ -166,6 +166,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
> [!注意]
> **:不正使用に関する免責事項** <br>

View File

@@ -66,19 +66,19 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -168,6 +168,7 @@ RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 클라이언트용 JavaScript
## 스크린샷

View File

@@ -62,19 +62,19 @@ Venligst last ned Sciters dynamiske bibliotek selv.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -163,6 +163,7 @@ Venligst pass på att du kjører disse kommandoene fra roten av RustDesk reposit
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
## Skjermbilder

View File

@@ -155,6 +155,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web
## Zrzuty ekranu

View File

@@ -64,19 +64,19 @@ Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -166,6 +166,7 @@ Certifique-se de executar esses comandos a partir da raiz do repositório do Rus
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
## Capturas de Tela

View File

@@ -66,19 +66,19 @@ Te rugăm să descarci singur librăria dinamică Sciter.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -168,6 +168,7 @@ Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, a
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
## Capturi de ecran

View File

@@ -59,7 +59,7 @@ RustDesk приветствует вклад каждого. Ознакомьт
- Выполните команду `cargo run`
## [Сборка](https://rustdesk.com/docs/en/dev/build/)
## [Сборка](https://rustdesk.com/docs/ru/dev/build/)
## Как собрать на Linux
@@ -68,19 +68,19 @@ RustDesk приветствует вклад каждого. Ознакомьт
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -170,6 +170,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
## Скриншоты
@@ -179,4 +180,4 @@ target/release/rustdesk
![Передача файлов](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP-туннелирование](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
![TCP-туннелирование](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -166,6 +166,7 @@ Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığın
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
## Ekran Görüntüleri

View File

@@ -59,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
@@ -160,6 +160,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
## Знімки екрана

View File

@@ -148,6 +148,7 @@ Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của th
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
## Snapshot

View File

@@ -220,6 +220,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图

View File

@@ -21,6 +21,18 @@
}
]
},
{
"name": "pam",
"buildsystem": "autotools",
"config-opts": ["--disable-selinux"],
"sources": [
{
"type": "archive",
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
}
]
},
{
"name": "rustdesk",
"buildsystem": "simple",
@@ -51,4 +63,4 @@
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Flatpak"
]
}
}

View File

@@ -84,6 +84,8 @@ const double _kPositionEpsilon = 1e-6;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
String get screenInfo => screenInfo_;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;

View File

@@ -936,19 +936,26 @@ void enterPasswordDialog(
);
}
void enterUserLoginDialog(SessionID sessionId,
OverlayDialogManager dialogManager, String osAccountDescTip) async {
void enterUserLoginDialog(
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
void enterUserLoginAndPasswordDialog(SessionID sessionId,
OverlayDialogManager dialogManager, String osAccountDescTip) async {
void enterUserLoginAndPasswordDialog(
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
@@ -956,6 +963,7 @@ void enterUserLoginAndPasswordDialog(SessionID sessionId,
osPasswordController: TextEditingController(),
passwordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
@@ -966,6 +974,7 @@ _connectDialog(
TextEditingController? osPasswordController,
TextEditingController? passwordController,
String? osAccountDescTip,
bool canRememberAccount = true,
}) async {
final errUsername = ''.obs;
var rememberPassword = false;
@@ -973,6 +982,11 @@ _connectDialog(
rememberPassword =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
var rememberAccount = false;
if (canRememberAccount && osUsernameController != null) {
rememberAccount =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
if (osUsernameController != null) {
osUsernameController.addListener(() {
if (errUsername.value.isNotEmpty) {
@@ -1000,6 +1014,12 @@ _connectDialog(
final osPassword = osPasswordController?.text.trim() ?? '';
final password = passwordController?.text.trim() ?? '';
if (passwordController != null && password.isEmpty) return;
if (rememberAccount) {
bind.sessionPeerOption(
sessionId: sessionId, name: 'os-username', value: osUsername);
bind.sessionPeerOption(
sessionId: sessionId, name: 'os-password', value: osPassword);
}
gFFI.login(
osUsername,
osPassword,
@@ -1076,6 +1096,16 @@ _connectDialog(
controller: osPasswordController,
autoFocus: false,
),
if (canRememberAccount)
rememberWidget(
translate('remember_account_tip'),
rememberAccount,
(v) {
if (v != null) {
setState(() => rememberAccount = v);
}
},
),
],
);
}
@@ -1512,6 +1542,91 @@ showSetOSPassword(
});
}
showSetOSAccount(
SessionID sessionId,
OverlayDialogManager dialogManager,
) async {
final usernameController = TextEditingController();
final passwdController = TextEditingController();
var username =
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-username') ??
'';
var password =
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
'';
usernameController.text = username;
passwdController.text = password;
dialogManager.show((setState, close, context) {
submit() {
final username = usernameController.text.trim();
final password = usernameController.text.trim();
bind.sessionPeerOption(
sessionId: sessionId, name: 'os-username', value: username);
bind.sessionPeerOption(
sessionId: sessionId, name: 'os-password', value: password);
close();
}
descWidget(String text) {
return Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
text,
maxLines: 3,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16),
),
),
Container(
height: 8,
),
],
);
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.password_rounded, color: MyTheme.accent),
Text(translate('OS Account')).paddingOnly(left: 10),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
descWidget(translate("os_account_desk_tip")),
DialogTextField(
title: translate(DialogTextField.kUsernameTitle),
controller: usernameController,
prefixIcon: DialogTextField.kUsernameIcon,
errorText: null,
),
PasswordWidget(controller: passwdController),
],
),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: close,
);
});
}
Widget buildNoteTextField({
required TextEditingController controller,
required VoidCallback onEscape,
@@ -1899,110 +2014,26 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}
int? _validateTrackpadSpeed(String text) {
final speed = int.tryParse(text);
if (speed == null || speed < kMinTrackpadSpeed || speed > kMaxTrackpadSpeed) {
BotToast.showText(
text:
'${translate('Invalid format')}: $kMinTrackpadSpeed-$kMaxTrackpadSpeed',
contentColor: Colors.red,
);
return null;
}
return speed;
}
Future<void> _saveTrackpadSpeed({
required SessionID sessionId,
required FFI ffi,
required int initSpeed,
required int speed,
}) async {
if (speed == initSpeed) {
return;
}
await bind.sessionSetTrackpadSpeed(sessionId: sessionId, value: speed);
await ffi.inputModel.updateTrackpadSpeed();
}
void _showTrackpadSpeedSaveError(Object error, StackTrace stackTrace) {
debugPrint('Failed to save trackpad speed: $error');
debugPrintStack(stackTrace: stackTrace);
BotToast.showText(
text: translate('Failed'),
contentColor: Colors.red,
);
}
List<Widget> _trackpadSpeedDialogActions({
required bool isSubmitting,
required VoidCallback close,
required VoidCallback submit,
}) {
return [
dialogButton(
'Cancel',
icon: Icon(Icons.close_rounded),
onPressed: isSubmitting ? null : close,
isOutline: true,
),
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: isSubmitting ? null : submit,
),
];
}
void trackpadSpeedDialog(SessionID sessionId, FFI ffi) {
final initSpeed = ffi.inputModel.trackpadSpeed;
trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
int initSpeed = ffi.inputModel.trackpadSpeed;
final curSpeed = SimpleWrapper(initSpeed);
var speedText = initSpeed.toString();
var isSubmitting = false;
ffi.dialogManager.show((setState, close, context) {
Future<void> submit([String? submittedText]) async {
if (isSubmitting) {
return;
}
speedText = submittedText ?? speedText;
final speed = _validateTrackpadSpeed(speedText);
if (speed == null) {
return;
}
setState(() => isSubmitting = true);
try {
await _saveTrackpadSpeed(
sessionId: sessionId,
ffi: ffi,
initSpeed: initSpeed,
speed: speed,
);
close();
} catch (error, stackTrace) {
_showTrackpadSpeedSaveError(error, stackTrace);
setState(() => isSubmitting = false);
}
final btnClose = dialogButton('Close', onPressed: () async {
if (curSpeed.value <= kMaxTrackpadSpeed &&
curSpeed.value >= kMinTrackpadSpeed &&
curSpeed.value != initSpeed) {
await bind.sessionSetTrackpadSpeed(
sessionId: sessionId, value: curSpeed.value);
await ffi.inputModel.updateTrackpadSpeed();
}
return CustomAlertDialog(
title: Text(
translate('Trackpad speed'),
style: TextStyle(fontSize: 21),
),
content: TrackpadSpeedWidget(
value: curSpeed,
onTextChanged: (text) => speedText = text,
onTextSubmitted: submit,
),
actions: _trackpadSpeedDialogActions(
isSubmitting: isSubmitting,
close: close,
submit: submit,
),
onSubmit: isSubmitting ? null : submit,
onCancel: isSubmitting ? null : close,
);
ffi.dialogManager.dismissAll();
});
msgBoxCommon(
ffi.dialogManager,
'Trackpad speed',
TrackpadSpeedWidget(
value: curSpeed,
),
[btnClose]);
}
void deleteConfirmDialog(Function onSubmit, String title) async {

View File

@@ -115,7 +115,6 @@ class _RawTouchGestureDetectorRegionState
InputModel get inputModel => widget.inputModel;
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
SessionID get sessionId => ffi.sessionId;
bool get canvasLocked => isMobile && ffi.canvasModel.locked;
@override
Widget build(BuildContext context) {
@@ -472,8 +471,6 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (canvasLocked) return;
if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale;

View File

@@ -253,18 +253,8 @@ class TrackpadSpeedWidget extends StatefulWidget {
final SimpleWrapper<int> value;
// If null, no debouncer will be applied.
final Function(int)? onDebouncer;
final ValueChanged<String>? onTextChanged;
// IME actions call TextField.onSubmitted without reaching the dialog's
// raw Enter handler, so the dialog needs a separate submission callback.
final ValueChanged<String>? onTextSubmitted;
TrackpadSpeedWidget({
Key? key,
required this.value,
this.onDebouncer,
this.onTextChanged,
this.onTextSubmitted,
});
TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer});
@override
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
@@ -286,34 +276,6 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
debouncerSpeed.setValue(value);
}
});
widget.onTextChanged?.call(_controller.text);
}
void updateTextValue(String text) {
widget.onTextChanged?.call(text);
final newValue = int.tryParse(text);
if (newValue == null ||
newValue < kMinTrackpadSpeed ||
newValue > kMaxTrackpadSpeed) {
return;
}
setState(() => value = newValue);
}
void submitTextValue(String text) {
final onTextSubmitted = widget.onTextSubmitted;
if (onTextSubmitted != null) {
onTextSubmitted(text);
return;
}
if (widget.onTextChanged != null) {
return;
}
final newValue = int.tryParse(text);
if (newValue == null) {
return;
}
updateValue(newValue);
}
@override
@@ -353,8 +315,12 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
controller: _controller,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
onChanged: updateTextValue,
onSubmitted: submitTextValue,
onSubmitted: (text) {
int? v = int.tryParse(text);
if (v != null) {
updateValue(v);
}
},
style: const TextStyle(fontSize: 13),
decoration: InputDecoration(
contentPadding:

View File

@@ -349,12 +349,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
showRequestElevationDialog(sessionId, ffi.dialogManager)),
);
}
// osPassword
// osAccount / osPassword
if (isDefaultConn && perms['keyboard'] != false) {
v.add(
TTextMenu(
child: Row(children: [
Text(translate('OS Password')),
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
]),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
@@ -363,12 +363,18 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
if (pi.isHeadless) {
showSetOSAccount(sessionId, ffi.dialogManager);
} else {
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
}
},
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
),
),
onPressed: () => handleOsPasswordAction(sessionId, ffi.dialogManager),
onPressed: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordAction(sessionId, ffi.dialogManager),
),
);
}

View File

@@ -18,6 +18,7 @@ const kKeyMapMode = 'map';
const kKeyTranslateMode = 'translate';
const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsIddImpl = "idd_impl";
const String kPlatformAdditionsRustDeskVirtualDisplays =
@@ -54,6 +55,7 @@ const String kAppTypeDesktopTerminal = "terminal";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowRefreshCurrentUser = "refresh_current_user";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowGetScreenList = "get_screen_list";
// This method is not used, maybe it can be removed.
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
@@ -86,6 +88,7 @@ const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render";
const String kOptionTextureRenderHealth = "texture-render-health";
const String kOptionD3DRender = "allow-d3d-render";
const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows";
@@ -162,6 +165,7 @@ const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
const String kOptionAllowAutoUpdate = "allow-auto-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
@@ -190,7 +194,6 @@ 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 kOptionHideGeneralSetting = "hide-general-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
@@ -323,11 +326,10 @@ double kNewWindowOffset = isWindows
? 30.0
: 50.0;
const kDragToResizeAreaPaddingSize = 5.0;
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(kDragToResizeAreaPaddingSize)
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0;

View File

@@ -16,6 +16,7 @@ import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
@@ -24,6 +25,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart';
import '../widgets/texture_render_probe.dart';
class DesktopHomePage extends StatefulWidget {
const DesktopHomePage({Key? key}) : super(key: key);
@@ -59,15 +61,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget build(BuildContext context) {
super.build(context);
final isIncomingOnly = bind.isIncomingOnly();
return _buildBlock(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
return Stack(
children: [
buildLeftPane(context),
if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
_buildBlock(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildLeftPane(context),
if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
],
)),
const Positioned(left: 0, top: 0, child: TextureRenderProbe()),
],
));
);
}
Widget _buildBlock({required Widget child}) {
@@ -110,6 +117,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
},
),
buildPluginEntry(),
];
if (isIncomingOnly) {
children.addAll([
@@ -780,6 +788,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
windowOnTop(null);
} else if (call.method == kWindowRefreshCurrentUser) {
gFFI.userModel.refreshCurrentUser();
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return '';
} else {
return jsonEncode(screenToMap(screen));
}
} else if (call.method == kWindowGetScreenList) {
return jsonEncode(
(await window_size.getScreenList()).map(screenToMap).toList());
@@ -881,6 +896,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
shouldBeBlocked(_block, canBeBlocked);
}
}
Widget buildPluginEntry() {
final entries = PluginUiManager.instance.entries.entries;
return Offstage(
offstage: entries.isEmpty,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...entries.map((entry) {
return entry.value;
})
],
),
);
}
}
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {

View File

@@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -53,6 +55,7 @@ enum SettingsTabKey {
safety,
network,
display,
plugin,
account,
printer,
about,
@@ -61,8 +64,7 @@ enum SettingsTabKey {
class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
if (bind.mainGetBuildinOption(key: kOptionHideGeneralSetting) != 'Y')
SettingsTabKey.general,
SettingsTabKey.general,
if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
@@ -72,9 +74,10 @@ class DesktopSettingPage extends StatefulWidget {
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
if (isWindows &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
SettingsTabKey.printer,
SettingsTabKey.about,
@@ -92,8 +95,7 @@ class DesktopSettingPage extends StatefulWidget {
if (index == -1) {
return;
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag) &&
Get.isRegistered<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag)) {
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller =
Get.find<PageController>(tag: _kSettingPageControllerTag);
@@ -161,23 +163,17 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
if (!mounted) {
return;
}
final blocked = await canBeBlocked();
if (!mounted) {
return;
}
_canBeBlocked.value = blocked;
_canBeBlocked.value = await canBeBlocked();
});
}
@override
void dispose() {
_videoConnTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
// Get.delete does not dispose a plain ChangeNotifier.
controller.dispose();
super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
WidgetsBinding.instance.removeObserver(this);
_videoConnTimer?.cancel();
}
List<_TabInfo> _settingTabs() {
@@ -200,6 +196,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
settingTabs.add(_TabInfo(tab, 'Display',
Icons.desktop_windows_outlined, Icons.desktop_windows));
break;
case SettingsTabKey.plugin:
settingTabs.add(_TabInfo(
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
break;
case SettingsTabKey.account:
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
@@ -233,6 +233,9 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
case SettingsTabKey.display:
children.add(const _Display());
break;
case SettingsTabKey.plugin:
children.add(const _Plugin());
break;
case SettingsTabKey.account:
children.add(const _Account());
break;
@@ -588,6 +591,10 @@ class _GeneralState extends State<_General> {
));
}
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
}
if (!bind.isDisableAccount()) {
children.add(_OptionCheckBox(
context,
@@ -2248,6 +2255,51 @@ class _CheckboxState extends State<_Checkbox> {
}
}
class _Plugin extends StatefulWidget {
const _Plugin({Key? key}) : super(key: key);
@override
State<_Plugin> createState() => _PluginState();
}
class _PluginState extends State<_Plugin> {
@override
Widget build(BuildContext context) {
bind.pluginListReload();
final scrollController = ScrollController();
return ChangeNotifierProvider.value(
value: pluginManager,
child: Consumer<PluginManager>(builder: (context, model, child) {
return ListView(
controller: scrollController,
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
).marginOnly(bottom: _kListViewBottomMargin);
}),
);
}
Widget pluginCard(PluginInfo plugin) {
return ChangeNotifierProvider.value(
value: plugin,
child: Consumer<PluginInfo>(
builder: (context, model, child) => DesktopSettingsCard(plugin: model),
),
);
}
Widget accountAction() {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty
? 'Login'
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: logOutConfirmDialog()
}));
}
}
class _Printer extends StatefulWidget {
const _Printer({super.key});

View File

@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart';
import 'macos_full_screen_focus_recovery.dart';
@@ -156,6 +157,7 @@ class _RemotePageState extends State<RemotePage>
widget.tabController?.state.listen(_onMacOSTabStateChanged);
}
Get.put<FFI>(_ffi, tag: widget.id);
RasterStallMonitor.start();
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
_ffi.canvasModel.activateLocalCursor();
showKBLayoutTypeChooserIfNeeded(
@@ -182,6 +184,7 @@ class _RemotePageState extends State<RemotePage>
WakelockManager.enable(_uniqueKey);
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -273,11 +276,6 @@ class _RemotePageState extends State<RemotePage>
tabState.tabs[selected].key == widget.id;
}
// Every Windows requestFocus() must pass this, or a blocking dialog or an
// inactive tab could hand remote input to this page.
bool get _windowsCanFocusRemoteInput =>
_isSelectedTab && _blockableOverlayState.middleBlocked.isFalse;
bool get _isMacOSKeyboardContextActive {
return stateGlobal.isFocused.value && !_isWindowBlur && _isSelectedTab;
}
@@ -518,15 +516,6 @@ class _RemotePageState extends State<RemotePage>
_queueMacOSKeyboardAfterFullScreen(allowHiddenLifecycle: true);
}
// Refocus without PointerEnter: the cursor already hovers the image when
// focus returns (Alt+Tab, taskbar), so enterView() never fires again.
if (isWindows &&
_cursorOverImage.value &&
_windowsCanFocusRemoteInput &&
!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
// Restore relative mouse mode constraints when window regains focus.
if (_ffi.inputModel.relativeMouseMode.value) {
if (isMacOS) {
@@ -537,7 +526,7 @@ class _RemotePageState extends State<RemotePage>
_cursorOverImage.value = true;
_macOSLocalFocusLost = false;
}
} else if (!isWindows || _windowsCanFocusRemoteInput) {
} else {
_rawKeyFocusNode.requestFocus();
}
_ffi.inputModel.onWindowFocus();
@@ -660,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
// Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
_ffi.textureModel.onRemotePageDispose(closeSession);
_ffi.textureModel.onRemotePageDispose();
if (closeSession && !isMacOS) {
// ensure we leave this session, this is a double check
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
@@ -849,16 +838,7 @@ class _RemotePageState extends State<RemotePage>
_macOSLocalFocusLost = false;
stateGlobal.getInputSource(force: true);
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
} else if (isWindows) {
// Blur unfocuses this node and nothing restores it, so the keyboard stayed
// dead until a click. Focus only while the window is really active, or a
// background window would grab system keys. onFocusChange does enterOrLeave.
if (!_isWindowBlur &&
_windowsCanFocusRemoteInput &&
!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
} else {
} else if (!isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
@@ -1424,3 +1404,4 @@ class CursorPaint extends StatelessWidget {
);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
import 'package:xterm/xterm.dart';
import 'terminal_connection_manager.dart';
class TerminalPage extends StatefulWidget {
@@ -197,7 +197,7 @@ class _TerminalPageState extends State<TerminalPage>
body: LayoutBuilder(
builder: (context, constraints) {
final heightPx = constraints.maxHeight;
return TerminalMouseInteraction(
return TerminalView(
_terminalModel.terminal,
controller: _terminalModel.terminalController,
focusNode: _terminalFocusNode,

View File

@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.initState();
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
RasterStallMonitor.start();
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -127,6 +129,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
WakelockManager.enable(_uniqueKey);
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
DesktopMultiWindow.addListener(this);
@@ -221,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession);
_ffi.textureModel.onViewCameraPageDispose();
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);

View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
/// Records a hung raster thread (frames continuously scheduled but no frame
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
/// rescued in-process, so the next launch defaults texture rendering off.
class RasterStallMonitor {
static bool _started = false;
static bool _reported = false;
static DateTime? _lastTimings;
static DateTime _lastQuiet = DateTime.now();
static void start() {
if (_started || isWeb) return;
_started = true;
SchedulerBinding.instance.addTimingsCallback((_) {
_lastTimings = DateTime.now();
});
Timer.periodic(const Duration(seconds: 2), (_) {
if (_reported) return;
final now = DateTime.now();
final lifecycle = SchedulerBinding.instance.lifecycleState;
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
// no timings is legitimate, keep moving the quiet anchor forward.
if (stateGlobal.isMinimized ||
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
!SchedulerBinding.instance.hasScheduledFrame) {
_lastQuiet = now;
return;
}
var ref = _lastQuiet;
final lastTimings = _lastTimings;
if (lastTimings != null && lastTimings.isAfter(ref)) {
ref = lastTimings;
}
if (now.difference(ref) > const Duration(seconds: 30)) {
_reported = true;
bind.mainSetLocalOption(
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
debugPrint(
'raster thread stall detected, texture rendering disabled for next launch');
}
});
}
}

View File

@@ -9,6 +9,9 @@ import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
import 'package:flutter_hbb/plugin/common.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -1333,12 +1336,6 @@ class ScreenAdjustor {
final FFI ffi;
final VoidCallback cbExitFullscreen;
window_size.Screen? _screen;
Size? _waylandMaximizedWorkAreaSize;
Rect? _waylandWorkAreaScreenFrame;
double? _waylandWorkAreaScaleFactor;
Rect? _x11WorkArea;
Rect? _x11WorkAreaScreenFrame;
double? _x11WorkAreaScaleFactor;
ScreenAdjustor({
required this.id,
@@ -1349,18 +1346,9 @@ class ScreenAdjustor {
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
int get windowId => stateGlobal.windowId;
Future<bool?> isWindowMaximized() async {
try {
return await WindowController.fromWindowId(windowId).isMaximized();
} catch (_) {
// The delayed resolution callback may run after the window is disposed.
return null;
}
}
adjustWindow(BuildContext context) {
return futureBuilder(
future: isWindowCanBeAdjusted(context),
future: isWindowCanBeAdjusted(),
hasData: (data) {
final visible = data as bool;
if (!visible) return Offstage();
@@ -1376,201 +1364,36 @@ class ScreenAdjustor {
});
}
// Linux screen and work-area coordinates can use different units or become
// unreliable across Wayland/X11 state changes, so normalize reported frames
// and cache usable work-area measurements before sizing the window.
Future<void> _updateLinuxWorkAreaCache({
required window_size.Screen screen,
required Rect wndRect,
required bool isWayland,
required bool isX11,
required bool forMenu,
}) async {
if (isWayland &&
(_waylandWorkAreaScreenFrame != screen.frame ||
_waylandWorkAreaScaleFactor != screen.scaleFactor)) {
_waylandMaximizedWorkAreaSize = null;
_waylandWorkAreaScreenFrame = screen.frame;
_waylandWorkAreaScaleFactor = screen.scaleFactor;
}
if (isWayland &&
forMenu &&
!isFullscreen &&
await isWindowMaximized() == true) {
_waylandMaximizedWorkAreaSize = wndRect.size;
}
if (isX11 &&
(_x11WorkAreaScreenFrame != screen.frame ||
_x11WorkAreaScaleFactor != screen.scaleFactor)) {
_x11WorkArea = null;
_x11WorkAreaScreenFrame = screen.frame;
_x11WorkAreaScaleFactor = screen.scaleFactor;
}
if (isX11 && forMenu && !isFullscreen) {
_x11WorkArea = screen.visibleFrame;
}
}
Future<Rect?> _getEffectiveScreenFrame({
required window_size.Screen screen,
required bool isWayland,
required bool isX11,
required bool forMenu,
}) async {
Rect frameRect = screen.visibleFrame;
if (isMacOS && forMenu && isFullscreen) {
List<double>? workArea;
try {
workArea = await kMacOSPermChannel
.invokeListMethod<double>('getMacOSWorkAreaSize');
} catch (_) {
return null;
}
if (workArea == null || workArea.length != 2) {
return null;
}
frameRect = Rect.fromLTWH(
frameRect.left,
frameRect.top,
workArea[0] < frameRect.width ? workArea[0] : frameRect.width,
workArea[1] < frameRect.height ? workArea[1] : frameRect.height,
);
}
final x11WorkArea = _x11WorkArea;
if (isX11 &&
forMenu &&
isFullscreen &&
x11WorkArea != null &&
(x11WorkArea.width < frameRect.width ||
x11WorkArea.height < frameRect.height)) {
frameRect = x11WorkArea;
}
final screenScale = screen.scaleFactor;
if (isWayland && screenScale > 1.01) {
String monitorLayoutMode;
try {
monitorLayoutMode =
await bind.mainGetCommon(key: 'gnome-monitor-layout-mode');
} catch (_) {
monitorLayoutMode = '';
}
if (monitorLayoutMode == 'physical') {
frameRect = Rect.fromLTRB(
frameRect.left / screenScale,
frameRect.top / screenScale,
frameRect.right / screenScale,
frameRect.bottom / screenScale,
);
}
}
return frameRect;
}
Future<Rect?> _getAdjustedWindowFrame(Size mediaSize,
{bool forMenu = false}) async {
final screen = _screen;
if (screen != null) {
// Windows window frames use physical pixels while Flutter view sizes are
// logical. macOS and Linux window frames use the same units as Flutter.
double scale = isWindows ? screen.scaleFactor : 1.0;
final Rect wndRect;
try {
wndRect = await WindowController.fromWindowId(windowId).getFrame();
} catch (e) {
debugPrint("Failed to get frame of window $windowId, it may be hidden");
return null;
}
// On Windows, wndRect is GetWindowRect while mediaSize is GetClientRect.
doAdjustWindow(BuildContext context) async {
await updateScreen();
if (_screen != null) {
cbExitFullscreen();
double scale = _screen!.scaleFactor;
final wndRect = await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = ffi.canvasModel;
// canvasModel.scale is the rendered scale and already applies kIgnoreDpi.
// Use it instead of the remote source resolution.
final isWayland = isLinux && bind.mainCurrentIsWayland();
final isX11 = isLinux && !isWayland;
await _updateLinuxWorkAreaCache(
screen: screen,
wndRect: wndRect,
isWayland: isWayland,
isX11: isX11,
forMenu: forMenu,
);
if (isWindows && forMenu && isFullscreen) {
// desktop_multi_window's hidden title bar keeps 8 physical pixels on
// each horizontal edge and at the bottom, plus up to 1px at the top.
// Fullscreen removes these in WM_NCCALCSIZE, so predict the restored
// frame's worst-case padding when deciding whether to show the menu.
magicWidth = 16.0;
magicHeight = 9.0;
}
double horizontalEdges;
double verticalEdges;
if (forMenu && (isLinux || ((isMacOS || isWindows) && isFullscreen))) {
// Linux Adjust Window unmaximizes; macOS and Windows exit fullscreen
// before resizing. Predict the restored normal-window edges when
// deciding whether to show the menu item.
final resizePadding = isLinux && !kUseCompatibleUiMode
? kDragToResizeAreaPaddingSize
: 0.0;
final windowEdge = kWindowBorderWidth + resizePadding;
horizontalEdges = windowEdge * 2;
verticalEdges = kDesktopRemoteTabBarHeight + windowEdge * 2;
} else {
horizontalEdges = CanvasModel.leftToEdge + CanvasModel.rightToEdge;
verticalEdges = CanvasModel.topToEdge + CanvasModel.bottomToEdge;
}
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
horizontalEdges) *
CanvasModel.leftToEdge +
CanvasModel.rightToEdge) *
scale +
magicWidth;
final height =
(canvasModel.getDisplayHeight() * canvasModel.scale + verticalEdges) *
scale +
magicHeight;
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
CanvasModel.topToEdge +
CanvasModel.bottomToEdge) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
final frameRect = await _getEffectiveScreenFrame(
screen: screen,
isWayland: isWayland,
isX11: isX11,
forMenu: forMenu,
);
if (frameRect == null) {
return null;
}
var availableSize = frameRect.size;
if (isWayland && forMenu && _waylandMaximizedWorkAreaSize != null) {
final cachedSize = _waylandMaximizedWorkAreaSize!;
availableSize = Size(
cachedSize.width < availableSize.width
? cachedSize.width
: availableSize.width,
cachedSize.height < availableSize.height
? cachedSize.height
: availableSize.height,
);
}
// A window frame cannot be smaller than its client area. Tolerate small
// floating-point differences; larger negative values mean the native
// frame and Flutter view metrics are not synchronized.
if (magicWidth < -0.1 || magicHeight < -0.1) {
return null;
}
// Reject implausibly small targets to avoid hiding the window.
if (width < 300 || height < 300) {
return null;
}
// The remote size may change after the menu is built. Reject targets
// that exceed the available area.
final exceedsScreen =
width > availableSize.width || height > availableSize.height;
if (exceedsScreen) {
return null;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
}
if (left < frameRect.left) {
left = frameRect.left;
@@ -1584,101 +1407,69 @@ class ScreenAdjustor {
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
return Rect.fromLTWH(left, top, width, height);
}
return null;
}
doAdjustWindow([BuildContext? context]) async {
// A resolution change is adjusted after a delay, when the menu context may
// already be disposed. Each desktop_multi_window window has its own engine,
// so that engine's first view is the current window.
final views = WidgetsBinding.instance.platformDispatcher.views;
if (context == null && views.isEmpty) {
return;
}
final view = context != null ? View.of(context) : views.first;
await updateScreen();
if (_screen != null) {
final wc = WindowController.fromWindowId(windowId);
final wasFullscreen = isFullscreen;
cbExitFullscreen();
if (wasFullscreen) {
// Wait for the native fullscreen exit to update the window frame.
await Future.delayed(Duration(milliseconds: 700));
await updateScreen();
}
if (isLinux) {
final isMaximized = await isWindowMaximized();
if (isMaximized == null) {
return;
}
if (isMaximized == true) {
// setFrame may be ignored while the native window is maximized.
try {
await wc.unmaximize();
} catch (_) {
return;
}
stateGlobal.setMaximized(false);
// Wait for the window manager and Flutter view metrics to reflect
// the restored window before calculating and setting its frame.
await Future.delayed(Duration(milliseconds: 300));
await updateScreen();
}
}
final mediaSize = MediaQueryData.fromView(view).size;
final frame = await _getAdjustedWindowFrame(mediaSize);
if (frame == null) {
return;
}
try {
await wc.setFrame(frame);
} catch (_) {
return;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
stateGlobal.setMaximized(false);
}
}
updateScreen() async {
_screen = await _getCurrentScreen();
}
Future<window_size.Screen?> _getCurrentScreen() async {
try {
return (await window_size.getWindowInfo()).screen;
} catch (e) {
debugPrint('Failed to get current window screen: $e');
return null;
final String info =
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
if (info.isEmpty) {
_screen = null;
} else {
final screenMap = jsonDecode(info);
_screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']),
Rect.fromLTRB(
screenMap['visibleFrame']['l'],
screenMap['visibleFrame']['t'],
screenMap['visibleFrame']['r'],
screenMap['visibleFrame']['b']),
screenMap['scaleFactor']);
}
}
Future<bool> isWindowCanBeAdjusted([BuildContext? context]) async {
if (isWeb) {
return false;
}
// Capture the view before awaiting because the menu context may be disposed.
final views = WidgetsBinding.instance.platformDispatcher.views;
if (context == null && views.isEmpty) {
return false;
}
final view = context != null ? View.of(context) : views.first;
final mediaSize = MediaQueryData.fromView(view).size;
_getScreenInfoDesktop() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
return v.result;
}
Future<bool> isWindowCanBeAdjusted() async {
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) {
return false;
}
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
if (!isWeb) {
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
}
await updateScreen();
if (_screen == null) {
return false;
}
return await _getAdjustedWindowFrame(mediaSize, forMenu: true) != null;
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
double selfWidth = _screen!.visibleFrame.width;
double selfHeight = _screen!.visibleFrame.height;
if (isFullscreen) {
selfWidth = _screen!.frame.width;
selfHeight = _screen!.frame.height;
}
final canvasModel = ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth =
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
final requiredHeight =
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale);
}
}
@@ -1687,11 +1478,20 @@ class _DisplayMenu extends StatefulWidget {
final FFI ffi;
final ToolbarState state;
final Function(bool) setFullscreen;
const _DisplayMenu(
{required this.id,
final Widget pluginItem;
_DisplayMenu(
{Key? key,
required this.id,
required this.ffi,
required this.state,
required this.setFullscreen});
required this.setFullscreen})
: pluginItem = LocationItem.createLocationItem(
id,
ffi,
kLocationClientRemoteToolbarDisplay,
true,
),
super(key: key);
@override
State<_DisplayMenu> createState() => _DisplayMenuState();
@@ -1729,6 +1529,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
_screenAdjustor.updateScreen();
menuChildrenGetter(_IconSubmenuButtonState state) {
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
@@ -1781,6 +1582,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
]);
}
}
if (ffi.connType == ConnType.defaultConn) {
menuChildren.add(widget.pluginItem);
}
return menuChildren;
}
@@ -2292,19 +2096,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
try {
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
} catch (e) {
debugPrint('Failed to get local resolution on Wayland: $e');
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
}
@@ -2376,16 +2176,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
return;
}
if (w == rect.width.toInt() && h == rect.height.toInt()) {
if (!await widget.screenAdjustor.isWindowCanBeAdjusted()) {
return;
}
if (widget.screenAdjustor.isFullscreen) {
return;
}
if ((await widget.screenAdjustor.isWindowMaximized()) == false) {
// This delayed callback can outlive the menu State, so its context
// is unsafe.
widget.screenAdjustor.doAdjustWindow();
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context);
}
}
});

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
_updateSessionsRenderVisible(false);
super.onWindowMinimize();
}
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
_updateSessionsRenderVisible(true);
super.onWindowMaximize();
}
@@ -402,9 +405,34 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
_updateSessionsRenderVisible(true);
super.onWindowUnmaximize();
}
@override
void onWindowRestore() {
// A plain restore (no maximize involved) must clear the minimized flag.
stateGlobal.setMinimized(false);
_updateSessionsRenderVisible(true);
super.onWindowRestore();
}
// A hidden window composites nothing; pause the Rust-side texture watchdog
// for its sessions so it cannot record a false failure.
void _updateSessionsRenderVisible(bool visible) {
if (tabType != DesktopTabType.remoteScreen &&
tabType != DesktopTabType.viewCamera) {
return;
}
for (final tab in controller.state.value.tabs) {
try {
final ffi = Get.find<FFI>(tag: tab.key);
bind.sessionSetRenderVisible(
sessionId: ffi.sessionId, visible: visible);
} catch (_) {}
}
}
_saveFrame({bool? flush}) async {
try {
if (tabType == DesktopTabType.main) {

View File

@@ -0,0 +1,172 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
/// Startup probe: renders one frame through a 1x1 external texture and
/// records in `texture-render-health` whether the engine consumed it, so a
/// broken environment is detected before the first session goes black.
class TextureRenderProbe extends StatefulWidget {
const TextureRenderProbe({Key? key}) : super(key: key);
@override
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
}
class _TextureRenderProbeState extends State<TextureRenderProbe> {
static bool _ranThisLaunch = false;
final _renderer = TextureRgbaRenderer();
int _textureId = -1;
int _textureKey = -1;
int _ptr = 0;
Timer? _timer;
int _ticks = 0;
bool _sawTimings = false;
bool _wasEffectiveOn = false;
DateTime? _lastTimings;
DateTime? _firstPush;
@override
void initState() {
super.initState();
if (_ranThisLaunch || isWeb || !isDesktop) return;
_ranThisLaunch = true;
if (bind.isIncomingOnly()) return;
// An old plugin without the consumed counter cannot be judged, and a
// recorded raster-stall means compositing a texture may hang this window.
if (!bind.mainTextureRenderProbeSupported()) return;
if (bind
.mainGetLocalOption(key: kOptionTextureRenderHealth)
.startsWith('failed-raster-stall')) {
return;
}
_wasEffectiveOn = bind.mainGetUseTextureRender();
// Only probe after the window has really rendered a frame: a hidden
// window (silent/tray start) must not record a false failure.
SchedulerBinding.instance.addTimingsCallback(_onTimings);
Future.delayed(const Duration(seconds: 5), () {
if (!_sawTimings) {
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
_finish(null);
}
});
}
void _onTimings(List<FrameTiming> timings) {
_lastTimings = DateTime.now();
if (_sawTimings) return;
_sawTimings = true;
_start();
}
void _start() async {
if (!mounted) return;
_textureKey = bind.getNextTextureKey();
final id = await _renderer.createTexture(_textureKey);
if (!mounted || id == -1) {
_finish(!mounted ? null : false);
return;
}
_ptr = await _renderer.getTexturePtr(_textureKey);
if (!mounted || _ptr <= 0) {
_finish(!mounted ? null : false);
return;
}
setState(() => _textureId = id);
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
_ticks += 1;
_firstPush ??= DateTime.now();
bind.mainPushTextureProbeFrame(ptr: _ptr);
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
// "Consumed" advances inside the plugin callback, before the GL/Metal
// upload; only a frame timing after the push proves a completed frame.
final frameCompleted = consumed &&
_lastTimings != null &&
_lastTimings!.isAfter(_firstPush!);
if (frameCompleted) {
_finish(true);
} else if (_ticks >= 10) {
if (consumed) {
_finish(null);
return;
}
// Only a window that is visibly compositing can prove a failure.
final lifecycle = SchedulerBinding.instance.lifecycleState;
final active =
lifecycle == null || lifecycle == AppLifecycleState.resumed;
final timingsFresh = _lastTimings != null &&
DateTime.now().difference(_lastTimings!) <
const Duration(milliseconds: 1500);
_finish(!stateGlobal.isMinimized && active && timingsFresh
? false
: null);
}
});
}
void _finish(bool? ok) {
_timer?.cancel();
_timer = null;
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
if (ok != null) {
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
if (ok) {
// This rgba probe disproves only the rgba black-texture class: gpu
// failures and raster stalls clear via the option toggle alone.
final clearable = old.isEmpty ||
old.startsWith('failed-probe') ||
old.startsWith('failed-watchdog-rgba');
if (clearable) {
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
}
} else if (!old.startsWith('failed')) {
debugPrint('texture render probe failed, disabling texture rendering');
bind.mainSetLocalOption(
key: kOptionTextureRenderHealth, value: 'failed-probe');
if (_wasEffectiveOn) {
showToast(translate('texture-render-fallback-tip'));
}
}
}
if (_textureKey != -1) {
_renderer.closeTexture(_textureKey);
_textureKey = -1;
}
_ptr = 0;
if (mounted && _textureId != -1) {
setState(() => _textureId = -1);
} else {
_textureId = -1;
}
}
@override
void dispose() {
_timer?.cancel();
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
if (_textureKey != -1) {
_renderer.closeTexture(_textureKey);
_textureKey = -1;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_textureId == -1) return const SizedBox.shrink();
// Must actually composite for the engine to sample the texture; the
// pushed pixel is fully transparent.
return IgnorePointer(
child: SizedBox(
width: 1, height: 1, child: Texture(textureId: _textureId)),
);
}
}

View File

@@ -30,6 +30,9 @@ import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
/// Basic window and launch properties.
int? kWindowId;
WindowType? kWindowType;
@@ -138,6 +141,8 @@ void runMainApp(bool startService) async {
await bind.mainCheckConnectStatus();
if (startService) {
gFFI.serverModel.startService();
bind.pluginSyncUi(syncTo: kAppTypeMain);
bind.pluginListReload();
}
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
@@ -565,6 +570,12 @@ _registerEventHandler() {
reloadAllWindows();
});
}
// Register native handlers.
if (isDesktop) {
platformFFI.registerEventHandler('native_ui', 'native_ui', (evt) async {
NativeUiHandler.instance.onEvent(evt);
});
}
if (isAndroid) {
platformFFI.registerEventHandler(
'android_needs_deploy', 'android_needs_deploy', (_) async {

View File

@@ -1276,14 +1276,6 @@ void showOptions(
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI);
if (isMobile) {
displayToggles.insert(
0,
TToggleMenu(
child: Text(translate('Lock canvas')),
value: gFFI.canvasModel.locked,
onChanged: (value) => gFFI.canvasModel.setLocked(value == true)));
}
List<TToggleMenu> privacyModeList = [];
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||

View File

@@ -16,6 +16,8 @@ class _PixelbufferTexture {
int _display = 0;
SessionID? _sessionId;
bool _destroying = false;
bool _closed = false;
int _ptr = 0;
int? _id;
final textureRenderer = TextureRgbaRenderer();
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
_textureKey = bind.getNextTextureKey();
_sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async {
final textureKey = _textureKey;
textureRenderer.createTexture(textureKey).then((id) async {
_id = id;
if (id != -1) {
if (_closed) {
// Destroyed while creation was still in flight (rapid
// connect/disconnect); nobody else will close this texture.
await textureRenderer.closeTexture(textureKey);
return;
}
ffi.textureModel.setRgbaTextureId(display: d, id: id);
final ptr = await textureRenderer.getTexturePtr(_textureKey);
final ptr = await textureRenderer.getTexturePtr(textureKey);
if (_closed) {
return;
}
_ptr = ptr;
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
debugPrint(
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
@@ -39,13 +52,16 @@ class _PixelbufferTexture {
});
}
destroy(bool unregisterTexture, FFI ffi) async {
destroy(FFI ffi) async {
_closed = true;
if (!_destroying && _textureKey != -1 && _sessionId != null) {
_destroying = true;
if (unregisterTexture) {
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
// sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
if (_ptr != 0) {
// Compare-and-clear: only clears if Rust still holds this pointer
// (#8016-safe); returning from this synchronous call also means no
// push through the old pointer is still in flight.
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
_ptr = 0;
}
await textureRenderer.closeTexture(_textureKey);
_textureKey = -1;
@@ -61,6 +77,7 @@ class _GpuTexture {
SessionID? _sessionId;
final support = bind.mainHasGpuTextureRender();
bool _destroying = false;
bool _closed = false;
int _display = 0;
int? _id;
int? _output;
@@ -79,9 +96,18 @@ class _GpuTexture {
gpuTextureRenderer.registerTexture().then((id) async {
_id = id;
if (id != null) {
if (_closed) {
// Destroyed while creation was still in flight (rapid
// connect/disconnect); nobody else will unregister this texture.
await gpuTextureRenderer.unregisterTexture(id);
return;
}
_textureId = id;
ffi.textureModel.setGpuTextureId(display: d, id: id);
final output = await gpuTextureRenderer.output(id);
if (_closed) {
return;
}
_output = output;
if (output != null) {
platformFFI.registerGpuTexture(sessionId, d, output);
@@ -95,20 +121,22 @@ class _GpuTexture {
}
}
destroy(bool unregisterTexture, FFI ffi) async {
destroy(FFI ffi) async {
// must stop texture render, render unregistered texture cause crash
_closed = true;
if (!_destroying && support && _sessionId != null && _textureId != -1) {
_destroying = true;
if (unregisterTexture) {
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
// sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
final output = _output;
if (output != null) {
// Compare-and-clear, see _PixelbufferTexture.destroy.
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
_output = null;
}
await gpuTextureRenderer.unregisterTexture(_textureId);
_textureId = -1;
_destroying = false;
debugPrint(
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
}
}
}
@@ -200,11 +228,11 @@ class TextureModel {
tryRemoveTexture(int idx) {
_control.remove(idx);
if (_pixelbufferRenderTextures.containsKey(idx)) {
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
_pixelbufferRenderTextures[idx]!.destroy(ffi);
_pixelbufferRenderTextures.remove(idx);
}
if (_gpuRenderTextures.containsKey(idx)) {
_gpuRenderTextures[idx]!.destroy(true, ffi);
_gpuRenderTextures[idx]!.destroy(ffi);
_gpuRenderTextures.remove(idx);
}
}
@@ -224,25 +252,25 @@ class TextureModel {
}
}
onRemotePageDispose(bool closeSession) async {
onRemotePageDispose() async {
final ffi = parent.target;
if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi);
await texture.destroy(ffi);
}
for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi);
await texture.destroy(ffi);
}
}
onViewCameraPageDispose(bool closeSession) async {
onViewCameraPageDispose() async {
final ffi = parent.target;
if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi);
await texture.destroy(ffi);
}
for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi);
await texture.destroy(ffi);
}
}

View File

@@ -1787,11 +1787,6 @@ class InputModel {
}
bool _checkPeerControlProtected(double x, double y) {
if (isViewOnly && showMyCursor) {
lastMousePos = ui.Offset(x, y);
return false;
}
final cursorModel = parent.target!.cursorModel;
if (cursorModel.isPeerControlProtected) {
lastMousePos = ui.Offset(x, y);

View File

@@ -25,6 +25,9 @@ import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:flutter_hbb/plugin/event.dart';
import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
@@ -434,6 +437,15 @@ class FfiModel with ChangeNotifier {
parent.target?.serverModel.updateVoiceCallState(evt);
} else if (name == 'fingerprint') {
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
} else if (name == 'plugin_manager') {
pluginManager.handleEvent(evt);
} else if (name == 'plugin_event') {
handlePluginEvent(evt,
(Map<String, dynamic> e) => handleMsgBox(e, sessionId, peerId));
} else if (name == 'plugin_reload') {
handleReloading(evt);
} else if (name == 'plugin_option') {
handleOption(evt);
} else if (name == "sync_peer_hash_password_to_personal_ab") {
if (desktopType == DesktopType.main || isWeb || isMobile) {
final id = evt['id'];
@@ -723,6 +735,11 @@ class FfiModel with ChangeNotifier {
_handleUseTextureRender(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
if (evt['reason'] == 'fallback') {
// The Rust watchdog detected that pushed frames were never rendered
// and switched this session to software rendering.
showToast(translate('texture-render-fallback-tip'));
}
waitForFirstImage.value = true;
isRefreshing = true;
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
@@ -908,12 +925,17 @@ class FfiModel with ChangeNotifier {
enter2FaDialog(sessionId, dialogManager);
} else if (type == 'input-password') {
enterPasswordDialog(sessionId, dialogManager);
} else if (type == 'session-login' || type == 'session-re-login') {
enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true);
} else if (type == 'session-login-password') {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'login_linux_tip', true);
} else if (type == 'terminal-admin-login') {
enterUserLoginDialog(
sessionId, dialogManager, 'terminal-admin-login-tip');
sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'terminal-admin-login-password') {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'terminal-admin-login-tip');
sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'restarting') {
// Treat restart messages as reconnect control events. Rust still sends
// title/text for legacy UI and translation reuse; Flutter keeps the last
@@ -2208,7 +2230,6 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
bool _locked = false;
double _devicePixelRatio = 1.0;
Size _size = Size.zero;
// the tabbar over the image
@@ -2257,19 +2278,12 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
bool get locked => _locked;
double get devicePixelRatio => _devicePixelRatio;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
RxBool get imageOverflow => _imageOverflow;
void setLocked(bool value) {
if (_locked == value) return;
_locked = value;
notifyListeners();
}
_resetScroll() => setScrollPercent(0.0, 0.0);
void setScrollPercent(double x, double y) {
@@ -2498,7 +2512,6 @@ class CanvasModel with ChangeNotifier {
}
void updateLocalCursor(double x, double y) {
if (parent.target?.ffiModel.viewOnly == true) return;
// If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null && parent.target!.ffiModel.keyboard) {
// Draw cursor if is not desktop.
@@ -2731,7 +2744,6 @@ class CanvasModel with ChangeNotifier {
_x = 0;
_y = 0;
_scale = 1.0;
_locked = false;
_lastViewStyle = ViewStyle.defaultViewStyle();
_timerMobileFocusCanvasCursor?.cancel();
_timerMobileRestoreCanvasOffset?.cancel();
@@ -2843,7 +2855,7 @@ class CursorData {
required this.width,
required this.height,
}) : hotx = hotxOrigin * scale,
hoty = hotyOrigin * scale;
hoty = hotxOrigin * scale;
int _doubleToInt(double v) => (v * 10e6).round().toInt();
@@ -4164,6 +4176,7 @@ class PeerInfo with ChangeNotifier {
RxBool isSet = false.obs;
bool get isWayland => platformAdditions[kPlatformAdditionsIsWayland] == true;
bool get isHeadless => platformAdditions[kPlatformAdditionsHeadless] == true;
bool get isInstalled =>
platform != kPeerPlatformWindows ||
platformAdditions[kPlatformAdditionsIsInstalled] == true;

View File

@@ -131,6 +131,12 @@ class PlatformFFI {
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr);
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionUnregisterPixelbufferTexture(
sessionId: sessionId, display: display, ptr: ptr);
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionUnregisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr);
/// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async {

View File

@@ -1,66 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:xterm/xterm.dart';
const _controlShiftVPasteShortcut = SingleActivator(
LogicalKeyboardKey.keyV,
control: true,
shift: true,
);
Future<void> writeTerminalClipboard(String text) async {
try {
await Clipboard.setData(ClipboardData(text: text));
} catch (error) {
debugPrint('[Terminal] Failed to write clipboard: $error');
}
}
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
if (defaultTargetPlatform != TargetPlatform.linux) return null;
return {
for (final entry in defaultTerminalShortcuts.entries)
if (!_isControlVShortcut(entry.key)) entry.key: entry.value,
_controlShiftVPasteShortcut:
const PasteTextIntent(SelectionChangedCause.keyboard),
};
}
bool _isControlVShortcut(ShortcutActivator shortcut) =>
shortcut is SingleActivator &&
shortcut.trigger == LogicalKeyboardKey.keyV &&
shortcut.control &&
!shortcut.shift &&
!shortcut.alt &&
!shortcut.meta;
FocusOnKeyEventCallback terminalCopyHandler(
Terminal terminal,
TerminalController controller,
) =>
(_, event) {
if (!_isWindowsCopyShortcut(event)) return KeyEventResult.ignored;
final selection = controller.selection;
if (selection == null || selection.isCollapsed) {
return KeyEventResult.ignored;
}
if (event is KeyDownEvent) {
final text = terminal.buffer.getText(selection);
unawaited(writeTerminalClipboard(text));
}
return KeyEventResult.handled;
};
bool _isWindowsCopyShortcut(KeyEvent event) {
final keyboard = HardwareKeyboard.instance;
return defaultTargetPlatform == TargetPlatform.windows &&
(event is KeyDownEvent || event is KeyRepeatEvent) &&
event.logicalKey == LogicalKeyboardKey.keyC &&
keyboard.isControlPressed &&
!keyboard.isShiftPressed &&
!keyboard.isAltPressed &&
!keyboard.isMetaPressed;
}

View File

@@ -1,235 +0,0 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:xterm/xterm.dart';
const _cellIndexOffset = 1;
const _legacyCodeOffset = 32;
const _leftButtonCode = 0;
const _motionButtonCode = 32;
const _releaseButtonCode = 3;
const _shiftModifierCode = 4;
const _metaModifierCode = 8;
const _controlModifierCode = 16;
const _modifierCodeMask =
_shiftModifierCode | _metaModifierCode | _controlModifierCode;
const _normalCoordinateLimit = 223;
const _utfCoordinateLimit = 2015;
String encodeTerminalMouseReport(
MouseReportMode mode,
int button,
CellOffset position, {
bool release = false,
}) {
final x = position.x + _cellIndexOffset;
final y = position.y + _cellIndexOffset;
final reportedButton =
release ? _releaseButtonCode | (button & _modifierCodeMask) : button;
switch (mode) {
case MouseReportMode.normal:
case MouseReportMode.utf:
final limit = mode == MouseReportMode.normal
? _normalCoordinateLimit
: _utfCoordinateLimit;
final encodedButton =
String.fromCharCode(_legacyCodeOffset + reportedButton);
return '\x1b[M$encodedButton${_legacyCoordinate(x, limit)}'
'${_legacyCoordinate(y, limit)}';
case MouseReportMode.sgr:
final suffix = release ? 'm' : 'M';
return '\x1b[<$button;$x;$y$suffix';
case MouseReportMode.urxvt:
return '\x1b[${_legacyCodeOffset + reportedButton};$x;${y}M';
}
}
String _legacyCoordinate(int value, int limit) =>
value > limit ? '\x00' : String.fromCharCode(_legacyCodeOffset + value);
int _activeModifierCode() {
final keyboard = HardwareKeyboard.instance;
return (keyboard.isShiftPressed ? _shiftModifierCode : 0) |
(keyboard.isAltPressed ? _metaModifierCode : 0) |
(keyboard.isControlPressed ? _controlModifierCode : 0);
}
class TerminalMouseDragReporter {
int? _pointerId;
TerminalController? _controller;
late CellOffset _lastReportedPosition;
var _ownsControllerSuspension = false;
var _releasePending = false;
var _reporting = false;
bool handleDown(
PointerDownEvent event,
Terminal terminal,
TerminalViewState? terminalView,
) {
if (!_isPrimaryMouse(event) || !_reportsDrag(terminal.mouseMode)) {
return false;
}
if (terminalView == null || terminalView.widget.readOnly) return false;
final controller = terminalView.widget.controller;
if (controller == null ||
controller.suspendedPointerInputs ||
!controller.pointerInput.inputs.contains(PointerInput.tap)) {
return false;
}
cancel();
_pointerId = event.pointer;
_controller = controller;
_ownsControllerSuspension = true;
_releasePending = true;
_reporting = true;
controller.setSuspendPointerInput(true);
_clearSelection(controller);
final position = _cellAt(event, terminalView);
_lastReportedPosition = position;
terminal.textInput(
_report(terminal.mouseReportMode, position),
);
return true;
}
bool handleMove(
PointerMoveEvent event,
Terminal terminal,
TerminalViewState? terminalView,
) {
if (event.pointer != _pointerId) return false;
if (terminalView == null) {
cancel();
return true;
}
final reportsDrag = _reportsDrag(terminal.mouseMode);
if (!_isPrimaryMouse(event)) {
if (_releasePending && reportsDrag) {
_reportRelease(
terminal,
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
);
}
cancel();
return true;
}
if (!_reporting || !reportsDrag) {
if (!reportsDrag) _releasePending = false;
_reporting = false;
// Keep ownership until the matching end event to suppress local selection.
final controller = _controller;
scheduleMicrotask(() => _clearSelection(controller));
return true;
}
final position = _cellAt(event, terminalView);
_lastReportedPosition = position;
terminal.textInput(
_report(terminal.mouseReportMode, position, motion: true),
);
final controller = _controller;
scheduleMicrotask(() => _clearSelection(controller));
return true;
}
bool handleEnd(
PointerEvent event,
Terminal terminal,
TerminalViewState? terminalView,
) {
if (event.pointer != _pointerId) return false;
if (terminalView != null &&
_releasePending &&
_reportsDrag(terminal.mouseMode)) {
_reportRelease(
terminal,
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
);
}
_clearSelection(_controller);
final controller = _controller;
_pointerId = null;
// Keep xterm's tap recognizer suspended for this pointer event.
scheduleMicrotask(() {
if (_pointerId == null && identical(_controller, controller)) {
_clearSelection(controller);
cancel();
}
});
return true;
}
void cancel() {
final controller = _controller;
if (_ownsControllerSuspension) {
controller?.setSuspendPointerInput(false);
}
_pointerId = null;
_controller = null;
_ownsControllerSuspension = false;
_releasePending = false;
_reporting = false;
}
void updateController(TerminalController controller) {
final oldController = _controller;
if (_pointerId == null || oldController == null) {
cancel();
return;
}
if (identical(oldController, controller)) return;
if (_ownsControllerSuspension) {
oldController.setSuspendPointerInput(false);
}
final acceptsPointerInput = !controller.suspendedPointerInputs &&
controller.pointerInput.inputs.contains(PointerInput.tap);
_controller = controller;
_ownsControllerSuspension = acceptsPointerInput;
_reporting = _reporting && acceptsPointerInput;
if (_ownsControllerSuspension) controller.setSuspendPointerInput(true);
_clearSelection(controller);
}
void _reportRelease(Terminal terminal, CellOffset position) {
terminal.textInput(
_report(
terminal.mouseReportMode,
position,
release: true,
),
);
}
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset(
renderTerminal.globalToLocal(event.position),
);
}
bool _isPrimaryMouse(PointerEvent event) =>
event.kind == PointerDeviceKind.mouse &&
(event.buttons & kPrimaryMouseButton) == kPrimaryMouseButton;
bool _reportsDrag(MouseMode mode) =>
mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove;
void _clearSelection(TerminalController? controller) {
if (controller == null || controller.selection == null) return;
controller.clearSelection();
}
String _report(
MouseReportMode mode,
CellOffset position, {
bool release = false,
bool motion = false,
}) {
final baseButton = motion ? _motionButtonCode : _leftButtonCode;
final button = baseButton | _activeModifierCode();
return encodeTerminalMouseReport(mode, button, position, release: release);
}
}

View File

@@ -1,19 +1,10 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:xterm/xterm.dart';
import 'terminal_copy_shortcut.dart';
import 'terminal_mouse_drag_reporter.dart';
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
/// modifier, so strict full-screen apps ignore the report and never scroll.
/// Upstream fix: TerminalStudio/xterm.dart#238.
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
const WheelButtonFixMouseHandler({this.positionProvider});
final CellOffset? Function()? positionProvider;
const WheelButtonFixMouseHandler();
@override
String? call(TerminalMouseEvent event) {
@@ -32,270 +23,20 @@ class WheelButtonFixMouseHandler implements TerminalMouseHandler {
String _reportWheel(TerminalMouseEvent event) {
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
final button = event.button.id - 4;
final position = positionProvider?.call() ?? event.position;
return encodeTerminalMouseReport(
event.state.mouseReportMode,
button,
position,
);
}
}
class TerminalMouseInteraction extends StatefulWidget {
const TerminalMouseInteraction(
this.terminal, {
super.key,
required this.controller,
this.focusNode,
this.backgroundOpacity = 1,
this.padding,
this.onSecondaryTapDown,
});
final Terminal terminal;
final TerminalController controller;
final FocusNode? focusNode;
final double backgroundOpacity;
final EdgeInsets? padding;
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
@override
State<TerminalMouseInteraction> createState() =>
_TerminalMouseInteractionState();
}
class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
static const _selectionScrollInterval = Duration(milliseconds: 50);
static const _noScroll = 0;
static const _scrollUp = -1;
static const _scrollDown = 1;
final _terminalViewKey = GlobalKey<TerminalViewState>();
final _scrollController = ScrollController();
final _mouseDrag = TerminalMouseDragReporter();
late final WheelButtonFixMouseHandler _mouseHandler;
TerminalMouseHandler? _previousMouseHandler;
Offset? _pointerPosition;
Offset? _selectionPointer;
CellAnchor? _selectionBase;
Buffer? _selectionBuffer;
int? _selectionPointerId;
Timer? _selectionScrollTimer;
var _selectionHasScrolled = false;
var _scrollDirection = _noScroll;
TerminalViewState? get _terminalView => _terminalViewKey.currentState;
@override
void initState() {
super.initState();
_mouseHandler = WheelButtonFixMouseHandler(
positionProvider: _cellAtPointer,
);
_installMouseHandler(widget.terminal);
}
@override
void didUpdateWidget(TerminalMouseInteraction oldWidget) {
super.didUpdateWidget(oldWidget);
final terminalChanged = !identical(oldWidget.terminal, widget.terminal);
final controllerChanged =
!identical(oldWidget.controller, widget.controller);
if (!terminalChanged && !controllerChanged) return;
if (controllerChanged && !terminalChanged) {
_mouseDrag.updateController(widget.controller);
} else {
_mouseDrag.cancel();
}
_clearSelectionDrag();
if (!terminalChanged) return;
_restoreMouseHandler(oldWidget.terminal);
_installMouseHandler(widget.terminal);
}
void _installMouseHandler(Terminal terminal) {
_previousMouseHandler = terminal.mouseHandler;
terminal.mouseHandler = _mouseHandler;
}
void _restoreMouseHandler(Terminal terminal) {
if (identical(terminal.mouseHandler, _mouseHandler)) {
terminal.mouseHandler = _previousMouseHandler;
}
}
CellOffset? _cellAtPointer() {
final terminalView = _terminalView;
final pointerPosition = _pointerPosition;
if (terminalView == null || pointerPosition == null) return null;
final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset(
renderTerminal.globalToLocal(pointerPosition),
);
}
void _updatePointerPosition(PointerEvent event) =>
_pointerPosition = event.position;
void _handlePointerDown(PointerDownEvent event) {
_updatePointerPosition(event);
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
_clearSelectionDrag();
return;
}
if (event.kind != PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
return;
}
_clearSelectionDrag();
final terminalView = _terminalView;
if (terminalView == null) return;
final renderTerminal = terminalView.renderTerminal;
final localPosition = renderTerminal.globalToLocal(event.position);
final selectionBuffer = widget.terminal.buffer;
_selectionPointerId = event.pointer;
_selectionBase = selectionBuffer.createAnchorFromOffset(
renderTerminal.getCellOffset(localPosition),
);
_selectionBuffer = selectionBuffer;
_selectionPointer = localPosition;
}
void _handlePointerMove(PointerMoveEvent event) {
_updatePointerPosition(event);
if (_mouseDrag.handleMove(event, widget.terminal, _terminalView)) return;
if (event.pointer != _selectionPointerId) return;
if (event.kind != PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
_clearSelectionDrag();
return;
}
final terminalView = _terminalView;
if (terminalView == null || _selectionBase == null) return;
final renderTerminal = terminalView.renderTerminal;
final localPosition = renderTerminal.globalToLocal(event.position);
_selectionPointer = localPosition;
_setScrollDirection(
_directionFor(localPosition, renderTerminal.paintBounds),
);
if (_selectionHasScrolled) {
scheduleMicrotask(() => _scrollSelection(scroll: false));
}
}
int _directionFor(Offset position, Rect bounds) {
if (position.dy < bounds.top) return _scrollUp;
if (position.dy >= bounds.bottom) return _scrollDown;
return _noScroll;
}
void _setScrollDirection(int direction) {
if (_scrollDirection == direction) return;
_stopAutoScroll();
_scrollDirection = direction;
if (direction == _noScroll) return;
_scrollSelection();
if (_scrollDirection != _noScroll) {
_selectionScrollTimer = Timer.periodic(
_selectionScrollInterval,
(_) => _scrollSelection(),
);
}
}
void _scrollSelection({bool scroll = true}) {
final terminalView = _terminalView;
final selectionBase = _selectionBase;
final selectionBuffer = _selectionBuffer;
final selectionPointer = _selectionPointer;
if (terminalView == null ||
selectionBase == null ||
selectionBuffer == null ||
selectionPointer == null ||
!_scrollController.hasClients) {
return;
}
if (!identical(selectionBuffer, widget.terminal.buffer) ||
!selectionBase.attached) {
_clearSelectionDrag();
return;
}
final renderTerminal = terminalView.renderTerminal;
if (scroll) {
final position = _scrollController.position;
final target =
(position.pixels + renderTerminal.lineHeight * _scrollDirection)
.clamp(position.minScrollExtent, position.maxScrollExtent)
.toDouble();
if (target == position.pixels) {
_stopAutoScroll();
} else {
position.jumpTo(target);
_selectionHasScrolled = true;
}
}
renderTerminal.selectCharacters(
renderTerminal.getOffset(selectionBase.offset),
selectionPointer,
);
}
void _handlePointerEnd(PointerEvent event) {
_updatePointerPosition(event);
if (!_mouseDrag.handleEnd(event, widget.terminal, _terminalView) &&
event.pointer != _selectionPointerId) return;
if (_selectionHasScrolled) _scrollSelection(scroll: false);
_clearSelectionDrag();
}
void _clearSelectionDrag() {
_selectionPointerId = null;
_selectionBase?.dispose();
_selectionBase = null;
_selectionBuffer = null;
_selectionPointer = null;
_selectionHasScrolled = false;
_stopAutoScroll();
}
void _stopAutoScroll() {
_selectionScrollTimer?.cancel();
_selectionScrollTimer = null;
_scrollDirection = _noScroll;
}
@override
void dispose() {
_mouseDrag.cancel();
_clearSelectionDrag();
_restoreMouseHandler(widget.terminal);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _handlePointerDown,
onPointerMove: _handlePointerMove,
onPointerUp: _handlePointerEnd,
onPointerHover: _updatePointerPosition,
onPointerCancel: _handlePointerEnd,
onPointerSignal: _updatePointerPosition,
onPointerPanZoomStart: _updatePointerPosition,
onPointerPanZoomUpdate: _updatePointerPosition,
onPointerPanZoomEnd: _updatePointerPosition,
child: TerminalView(
widget.terminal,
key: _terminalViewKey,
controller: widget.controller,
scrollController: _scrollController,
focusNode: widget.focusNode,
backgroundOpacity: widget.backgroundOpacity,
padding: widget.padding,
shortcuts: platformTerminalShortcuts(),
onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller),
onSecondaryTapDown: widget.onSecondaryTapDown,
),
);
final x = event.position.x + 1;
final y = event.position.y + 1;
switch (event.state.mouseReportMode) {
case MouseReportMode.normal:
case MouseReportMode.utf:
final limit =
event.state.mouseReportMode == MouseReportMode.normal ? 223 : 2015;
final col = x > limit ? '\x00' : String.fromCharCode(32 + x);
final row = y > limit ? '\x00' : String.fromCharCode(32 + y);
return '\x1b[M${String.fromCharCode(32 + button)}$col$row';
case MouseReportMode.sgr:
return '\x1b[<$button;$x;${y}M';
case MouseReportMode.urxvt:
return '\x1b[${32 + button};$x;${y}M';
}
}
}

View File

@@ -136,6 +136,12 @@ class PlatformFFI {
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr);
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionUnregisterPixelbufferTexture(
sessionId: sessionId, display: display, ptr: ptr);
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionUnregisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr);
Future<void> init(String appType) async {
Completer completer = Completer();

View File

@@ -10,6 +10,8 @@ final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => '';
final isWebOnWindows_ = false;
final isWebOnLinux_ = false;
final isWebOnMacOS_ = false;

View File

@@ -0,0 +1,42 @@
import 'dart:convert';
typedef PluginId = String;
// ui location
const String kLocationHostMainPlugin = 'host|main|settings|plugin';
const String kLocationClientRemoteToolbarDisplay =
'client|remote|toolbar|display';
class MsgFromUi {
String id;
String name;
String location;
String key;
String value;
String action;
MsgFromUi({
required this.id,
required this.name,
required this.location,
required this.key,
required this.value,
required this.action,
});
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'name': name,
'location': location,
'key': key,
'value': value,
'action': action,
};
}
@override
String toString() {
return jsonEncode(toJson());
}
}

View File

@@ -0,0 +1,18 @@
import 'dart:convert';
import 'package:flutter/material.dart';
void handlePluginEvent(
Map<String, dynamic> evt,
Function(Map<String, dynamic> e) handleMsgBox,
) {
Map<String, dynamic>? content;
try {
content = json.decode(evt['content']);
} catch (e) {
debugPrint(
'Json decode plugin event content failed: $e, ${evt['content']}');
}
if (content?['t'] == 'MsgBox') {
handleMsgBox(content?['c']);
}
}

View File

@@ -0,0 +1,79 @@
import 'dart:convert';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/plugin/utils/dialogs.dart';
abstract class NativeHandler {
bool onEvent(Map<String, dynamic> evt);
}
typedef OnSelectPeersCallback = Bool Function(Int returnCode,
Pointer<Void> data, Uint64 dataLength, Pointer<Void> userData);
typedef OnSelectPeersCallbackDart = bool Function(
int returnCode, Pointer<Void> data, int dataLength, Pointer<Void> userData);
class NativeUiHandler extends NativeHandler {
NativeUiHandler._();
static NativeUiHandler instance = NativeUiHandler._();
@override
bool onEvent(Map<String, dynamic> evt) {
final name = evt['name'];
final action = evt['action'];
if (name != "native_ui") {
return false;
}
switch (action) {
case "select_peers":
int cb = evt['cb'];
int userData = evt['user_data'] ?? 0;
final cbFuncNative = Pointer.fromAddress(cb)
.cast<NativeFunction<OnSelectPeersCallback>>();
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
onSelectPeers(cbFuncDart, userData);
break;
case "register_ui_entry":
int cb = evt['on_tap_cb'];
int userData = evt['user_data'] ?? 0;
String title = evt['title'] ?? "";
final cbFuncNative = Pointer.fromAddress(cb)
.cast<NativeFunction<OnSelectPeersCallback>>();
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
onRegisterUiEntry(title, cbFuncDart, userData);
break;
default:
return false;
}
return true;
}
void onSelectPeers(OnSelectPeersCallbackDart cb, int userData) async {
showPeerSelectionDialog(onPeersCallback: (peers) {
String json = jsonEncode(<String, dynamic> {
"peers": peers
});
final native = json.toNativeUtf8();
cb(0, native.cast(), native.length, Pointer.fromAddress(userData));
malloc.free(native);
});
}
void onRegisterUiEntry(String title, OnSelectPeersCallbackDart cbFuncDart, int userData) {
Widget widget = InkWell(
child: Container(
height: 25.0,
child: Row(
children: [
Expanded(child: Text(title)),
Icon(Icons.chevron_right_rounded, size: 12.0,)
],
),
),
);
PluginUiManager.instance.registerEntry(title, widget);
}
}

View File

@@ -0,0 +1,319 @@
// The plugin manager is a singleton class that manages the plugins.
// 1. It merge metadata and the desc of plugins.
import 'dart:convert';
import 'dart:collection';
import 'package:flutter/material.dart';
const String kValueTrue = '1';
const String kValueFalse = '0';
class ConfigItem {
String key;
String description;
String defaultValue;
ConfigItem(this.key, this.defaultValue, this.description);
ConfigItem.fromJson(Map<String, dynamic> json)
: key = json['key'] ?? '',
description = json['description'] ?? '',
defaultValue = json['default'] ?? '';
static String get trueValue => kValueTrue;
static String get falseValue => kValueFalse;
static bool isTrue(String value) => value == kValueTrue;
static bool isFalse(String value) => value == kValueFalse;
}
class UiType {
String key;
String text;
String tooltip;
String action;
UiType(this.key, this.text, this.tooltip, this.action);
UiType.fromJson(Map<String, dynamic> json)
: key = json['key'] ?? '',
text = json['text'] ?? '',
tooltip = json['tooltip'] ?? '',
action = json['action'] ?? '';
static UiType? create(Map<String, dynamic> json) {
if (json['t'] == 'Button') {
return UiButton.fromJson(json['c']);
} else if (json['t'] == 'Checkbox') {
return UiCheckbox.fromJson(json['c']);
} else {
return null;
}
}
}
class UiButton extends UiType {
String icon;
UiButton(
{required String key,
required String text,
required this.icon,
required String tooltip,
required String action})
: super(key, text, tooltip, action);
UiButton.fromJson(Map<String, dynamic> json)
: icon = json['icon'] ?? '',
super.fromJson(json);
}
class UiCheckbox extends UiType {
UiCheckbox(
{required String key,
required String text,
required String tooltip,
required String action})
: super(key, text, tooltip, action);
UiCheckbox.fromJson(Map<String, dynamic> json) : super.fromJson(json);
}
class Location {
// location key:
// host|main|settings|plugin
// client|remote|toolbar|display
HashMap<String, UiType> ui;
Location(this.ui);
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
(json['ui'] as Map<String, dynamic>).forEach((key, value) {
var ui = UiType.create(value);
if (ui != null) {
this.ui[ui.key] = ui;
}
});
}
}
class PublishInfo {
PublishInfo({
required this.lastReleased,
required this.published,
});
final DateTime lastReleased;
final DateTime published;
}
class Meta {
Meta({
required this.id,
required this.name,
required this.version,
required this.description,
required this.author,
required this.home,
required this.license,
required this.publishInfo,
required this.source,
});
final String id;
final String name;
final String version;
final String description;
final String author;
final String home;
final String license;
final PublishInfo publishInfo;
final String source;
}
class SourceInfo {
String name; // 1. RustDesk github 2. Local
String url;
String description;
SourceInfo({
required this.name,
required this.url,
required this.description,
});
}
class PluginInfo with ChangeNotifier {
SourceInfo sourceInfo;
Meta meta;
String installedVersion; // It is empty if not installed.
String failedMsg;
String invalidReason; // It is empty if valid.
PluginInfo({
required this.sourceInfo,
required this.meta,
required this.installedVersion,
required this.invalidReason,
this.failedMsg = '',
});
bool get installed => installedVersion.isNotEmpty;
bool get needUpdate => installed && installedVersion != meta.version;
void setInstall(String msg) {
if (msg == "finished") {
msg = '';
}
failedMsg = msg;
if (msg.isEmpty) {
installedVersion = meta.version;
}
notifyListeners();
}
void setUninstall(String msg) {
failedMsg = msg;
if (msg.isEmpty) {
installedVersion = '';
}
notifyListeners();
}
}
class PluginManager with ChangeNotifier {
String failedReason = ''; // The reason of failed to load plugins.
final List<PluginInfo> _plugins = [];
PluginManager._();
static final PluginManager _instance = PluginManager._();
static PluginManager get instance => _instance;
List<PluginInfo> get plugins => _plugins;
PluginInfo? getPlugin(String id) {
for (var p in _plugins) {
if (p.meta.id == id) {
return p;
}
}
return null;
}
void handleEvent(Map<String, dynamic> evt) {
if (evt['plugin_list'] != null) {
_handlePluginList(evt['plugin_list']);
} else if (evt['plugin_install'] != null && evt['id'] != null) {
_handlePluginInstall(evt['id'], evt['plugin_install']);
} else if (evt['plugin_uninstall'] != null && evt['id'] != null) {
_handlePluginUninstall(evt['id'], evt['plugin_uninstall']);
} else {
debugPrint('Failed to handle manager event: $evt');
}
}
void _sortPlugins() {
plugins.sort((a, b) {
if (a.installed) {
return -1;
} else if (b.installed) {
return 1;
} else {
return 0;
}
});
}
void _handlePluginList(String pluginList) {
_plugins.clear();
try {
for (var p in json.decode(pluginList) as List<dynamic>) {
final plugin = _getPluginFromEvent(p);
if (plugin == null) {
continue;
}
_plugins.add(plugin);
}
} catch (e) {
debugPrint('Failed to decode $e, plugin list \'$pluginList\'');
}
_sortPlugins();
notifyListeners();
}
void _handlePluginInstall(String id, String msg) {
debugPrint('Plugin \'$id\' install msg $msg');
for (var i = 0; i < _plugins.length; i++) {
if (_plugins[i].meta.id == id) {
_plugins[i].setInstall(msg);
_sortPlugins();
notifyListeners();
return;
}
}
}
void _handlePluginUninstall(String id, String msg) {
debugPrint('Plugin \'$id\' uninstall msg $msg');
for (var i = 0; i < _plugins.length; i++) {
if (_plugins[i].meta.id == id) {
_plugins[i].setUninstall(msg);
_sortPlugins();
notifyListeners();
return;
}
}
}
PluginInfo? _getPluginFromEvent(Map<String, dynamic> evt) {
final s = evt['source'];
assert(s != null, 'Source is null');
if (s == null) {
return null;
}
final source = SourceInfo(
name: s['name'],
url: s['url'] ?? '',
description: s['description'] ?? '',
);
final m = evt['meta'];
assert(m != null, 'Meta is null');
if (m == null) {
return null;
}
late DateTime lastReleased;
late DateTime published;
try {
lastReleased = DateTime.parse(
m['publish_info']?['last_released'] ?? '1970-01-01T00+00:00');
} catch (e) {
lastReleased = DateTime.utc(1970);
}
try {
published = DateTime.parse(
m['publish_info']?['published'] ?? '1970-01-01T00+00:00');
} catch (e) {
published = DateTime.utc(1970);
}
final meta = Meta(
id: m['id'],
name: m['name'],
version: m['version'],
description: m['description'] ?? '',
author: m['author'],
home: m['home'] ?? '',
license: m['license'] ?? '',
source: m['source'] ?? '',
publishInfo:
PublishInfo(lastReleased: lastReleased, published: published),
);
return PluginInfo(
sourceInfo: source,
meta: meta,
installedVersion: evt['installed_version'],
invalidReason: evt['invalid_reason'] ?? '',
);
}
}
PluginManager get pluginManager => PluginManager.instance;

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import './common.dart';
import './manager.dart';
final Map<String, LocationModel> _locationModels = {};
final Map<String, OptionModel> _optionModels = {};
class OptionModel with ChangeNotifier {
String? v;
String? get value => v;
set value(String? v) {
this.v = v;
notifyListeners();
}
static String key(String location, PluginId id, String peer, String k) =>
'$location|$id|$peer|$k';
}
class PluginModel with ChangeNotifier {
final List<UiType> uiList = [];
final Map<String, String> opts = {};
void add(List<UiType> uiList) {
bool found = false;
for (var ui in uiList) {
for (int i = 0; i < this.uiList.length; i++) {
if (this.uiList[i].key == ui.key) {
this.uiList[i] = ui;
found = true;
}
}
if (!found) {
this.uiList.add(ui);
}
}
notifyListeners();
}
String? getOpt(String key) => opts.remove(key);
bool get isEmpty => uiList.isEmpty;
}
class LocationModel with ChangeNotifier {
final Map<PluginId, PluginModel> pluginModels = {};
void add(PluginId id, List<UiType> uiList) {
if (pluginModels[id] != null) {
pluginModels[id]!.add(uiList);
} else {
var model = PluginModel();
model.add(uiList);
pluginModels[id] = model;
notifyListeners();
}
}
void clear() {
pluginModels.clear();
notifyListeners();
}
void remove(PluginId id) {
pluginModels.remove(id);
notifyListeners();
}
bool get isEmpty => pluginModels.isEmpty;
}
void addLocationUi(String location, PluginId id, List<UiType> uiList) {
if (_locationModels[location] == null) {
_locationModels[location] = LocationModel();
}
_locationModels[location]?.add(id, uiList);
}
LocationModel? getLocationModel(String location) => _locationModels[location];
PluginModel? getPluginModel(String location, PluginId id) =>
_locationModels[location]?.pluginModels[id];
void clearPlugin(PluginId pluginId) {
for (var element in _locationModels.values) {
element.remove(pluginId);
}
}
void clearLocations() {
for (var element in _locationModels.values) {
element.clear();
}
}
OptionModel getOptionModel(
String location, PluginId pluginId, String peer, String key) {
final k = OptionModel.key(location, pluginId, peer, key);
if (_optionModels[k] == null) {
_optionModels[k] = OptionModel();
}
return _optionModels[k]!;
}
void updateOption(
String location, PluginId id, String peer, String key, String value) {
final k = OptionModel.key(location, id, peer, key);
_optionModels[k]?.value = value;
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class PluginUiManager {
PluginUiManager._();
static PluginUiManager instance = PluginUiManager._();
Map<String, Widget> entries = <String, Widget>{};
void registerEntry(String key, Widget widget) {
entries[key] = widget;
}
void unregisterEntry(String key) {
entries.remove(key);
}
}

View File

@@ -0,0 +1,86 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
void showPeerSelectionDialog(
{bool singleSelection = false,
required Function(List<String>) onPeersCallback}) async {
// load recent peers, we can directly use the peers in `gFFI.recentPeersModel`.
// The plugin is not used for now, so just left it empty here.
final peers = '';
if (peers.isEmpty) {
// debugPrint("load recent peers failed.");
return;
}
Map<String, dynamic> map = jsonDecode(peers);
List<dynamic> peersList = map['peers'] ?? [];
final selected = List<String>.empty(growable: true);
submit() async {
onPeersCallback.call(selected);
}
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title:
Text(translate(singleSelection ? "Select peers" : "Select a peer")),
content: SizedBox(
height: 300.0,
child: ListView.builder(
itemBuilder: (context, index) {
final Map<String, dynamic> peer = peersList[index];
final String platform = peer['platform'] ?? "";
final String id = peer['id'] ?? "";
final String alias = peer['alias'] ?? "";
return GestureDetector(
onTap: () {
setState(() {
if (selected.contains(id)) {
selected.remove(id);
} else {
selected.add(id);
}
});
},
child: Container(
key: ValueKey(index),
height: 50.0,
decoration: BoxDecoration(
color: Theme.of(context).highlightColor,
borderRadius: BorderRadius.circular(12.0)),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
margin: EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
// platform
SizedBox(
width: 8.0,
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
getPlatformImage(platform, size: 34.0),
],
),
SizedBox(
width: 8.0,
),
// id/alias
Expanded(child: Text(alias.isEmpty ? id : alias)),
],
),
),
);
},
itemCount: peersList.length,
itemExtent: 50.0,
),
),
onSubmit: submit,
);
});
}

View File

@@ -0,0 +1,301 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:provider/provider.dart';
import 'package:get/get.dart';
// to-do: do not depend on desktop
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import '../manager.dart';
import '../model.dart';
import '../common.dart';
// dup to flutter\lib\desktop\pages\desktop_setting_page.dart
const double _kCheckBoxLeftMargin = 10;
class LocationItem extends StatelessWidget {
final String peerId;
final FFI ffi;
final String location;
final LocationModel locationModel;
final bool isMenu;
LocationItem({
Key? key,
required this.peerId,
required this.ffi,
required this.location,
required this.locationModel,
required this.isMenu,
}) : super(key: key);
bool get isEmpty => locationModel.isEmpty;
static Widget createLocationItem(
String peerId, FFI ffi, String location, bool isMenu) {
final model = getLocationModel(location);
return model == null
? Container()
: LocationItem(
peerId: peerId,
ffi: ffi,
location: location,
locationModel: model,
isMenu: isMenu,
);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: locationModel,
child: Consumer<LocationModel>(builder: (context, model, child) {
return Column(
children: model.pluginModels.entries
.map((entry) => _buildPluginItem(entry.key, entry.value))
.toList(),
);
}),
);
}
Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem(
pluginId: id,
peerId: peerId,
ffi: ffi,
location: location,
pluginModel: model,
isMenu: isMenu,
);
}
class PluginItem extends StatelessWidget {
final PluginId pluginId;
final String peerId;
final FFI? ffi;
final String location;
final PluginModel pluginModel;
final bool isMenu;
PluginItem({
Key? key,
required this.pluginId,
required this.peerId,
this.ffi,
required this.location,
required this.pluginModel,
required this.isMenu,
}) : super(key: key);
bool get isEmpty => pluginModel.isEmpty;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: pluginModel,
child: Consumer<PluginModel>(
builder: (context, pluginModel, child) {
return Column(
children: pluginModel.uiList.map((ui) => _buildItem(ui)).toList(),
);
},
),
);
}
Widget _buildItem(UiType ui) {
Widget? child;
switch (ui.runtimeType) {
case UiButton:
if (isMenu) {
if (ffi != null) {
child = _buildMenuButton(ui as UiButton, ffi!);
}
} else {
child = _buildButton(ui as UiButton);
}
break;
case UiCheckbox:
if (isMenu) {
if (ffi != null) {
child = _buildCheckboxMenuButton(ui as UiCheckbox, ffi!);
}
} else {
child = _buildCheckbox(ui as UiCheckbox);
}
break;
default:
break;
}
// to-do: add plugin icon and tooltip
return child ?? Container();
}
Widget _buildButton(UiButton ui) {
return TextButton(
onPressed: () => bind.pluginEvent(
id: pluginId,
peer: peerId,
event: _makeEvent(ui.key),
),
child: Text(ui.text),
);
}
Widget _buildCheckbox(UiCheckbox ui) {
getChild(OptionModel model) {
final v = _getOption(model, ui.key);
if (v == null) {
// session or plugin not found
return Container();
}
onChanged(bool value) {
bind.pluginEvent(
id: pluginId,
peer: peerId,
event: _makeEvent(ui.key, v: value),
);
}
final value = ConfigItem.isTrue(v);
return GestureDetector(
child: Row(
children: [
Checkbox(
value: value,
onChanged: (_) => onChanged(!value),
).marginOnly(right: 5),
Expanded(
child: Text(translate(ui.text)),
)
],
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: () => onChanged(!value),
);
}
return ChangeNotifierProvider.value(
value: getOptionModel(location, pluginId, peerId, ui.key),
child: Consumer<OptionModel>(
builder: (context, model, child) => getChild(model),
),
);
}
Widget _buildCheckboxMenuButton(UiCheckbox ui, FFI ffi) {
getChild(OptionModel model) {
final v = _getOption(model, ui.key);
if (v == null) {
// session or plugin not found
return Container();
}
return CkbMenuButton(
value: ConfigItem.isTrue(v),
onChanged: (v) {
if (v != null) {
bind.pluginEvent(
id: pluginId,
peer: peerId,
event: _makeEvent(ui.key, v: v),
);
}
},
// to-do: RustDesk translate or plugin translate ?
child: Text(ui.text),
ffi: ffi,
);
}
return ChangeNotifierProvider.value(
value: getOptionModel(location, pluginId, peerId, ui.key),
child: Consumer<OptionModel>(
builder: (context, model, child) => getChild(model),
),
);
}
Widget _buildMenuButton(UiButton ui, FFI ffi) {
return MenuButton(
onPressed: () => bind.pluginEvent(
id: pluginId,
peer: peerId,
event: _makeEvent(ui.key),
),
// to-do: support trailing icon, but it will cause tree shake error.
// ```
// This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations:
// Target release_macos_bundle_flutter_assets failed: Exception: Avoid non-constant invocations of IconData or try to build again with --no-tree-shake-icons.
// ```
//
// trailingIcon: Icon(
// IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')),
//
// to-do: RustDesk translate or plugin translate ?
child: Text(ui.text),
ffi: ffi,
);
}
Uint8List _makeEvent(
String key, {
bool? v,
}) {
final event = MsgFromUi(
id: pluginId,
name: pluginManager.getPlugin(pluginId)?.meta.name ?? '',
location: location,
key: key,
value:
v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '',
action: '',
);
return Uint8List.fromList(event.toString().codeUnits);
}
String? _getOption(OptionModel model, String key) {
var v = model.value;
if (v == null) {
try {
if (peerId.isEmpty) {
v = bind.pluginGetSharedOption(id: pluginId, key: key);
} else {
v = bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: key);
}
} catch (e) {
debugPrint('Failed to get option "$key", $e');
v = null;
}
}
return v;
}
}
void handleReloading(Map<String, dynamic> evt) {
if (evt['id'] == null || evt['location'] == null) {
return;
}
try {
final uiList = <UiType>[];
for (var e in json.decode(evt['ui'] as String)) {
final ui = UiType.create(e);
if (ui != null) {
uiList.add(ui);
}
}
if (uiList.isNotEmpty) {
addLocationUi(evt['location']!, evt['id']!, uiList);
}
} catch (e) {
debugPrint('Failed handleReloading, json decode of ui, $e ');
}
}
void handleOption(Map<String, dynamic> evt) {
updateOption(
evt['location'], evt['id'], evt['peer'] ?? '', evt['key'], evt['value']);
}

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/plugin/model.dart';
import 'package:flutter_hbb/plugin/common.dart';
import 'package:get/get.dart';
import '../manager.dart';
import './desc_ui.dart';
// to-do: use settings from desktop_setting_page.dart
const double _kCardFixedWidth = 540;
const double _kCardLeftMargin = 15;
const double _kContentHMargin = 15;
const double _kTitleFontSize = 20;
const double _kVersionFontSize = 12;
class DesktopSettingsCard extends StatefulWidget {
final PluginInfo plugin;
DesktopSettingsCard({
Key? key,
required this.plugin,
}) : super(key: key);
@override
State<DesktopSettingsCard> createState() => _DesktopSettingsCardState();
}
class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
PluginInfo get plugin => widget.plugin;
bool get installed => plugin.installed;
bool isEnabled = false;
@override
Widget build(BuildContext context) {
isEnabled = bind.pluginIsEnabled(id: plugin.meta.id);
return Row(
children: [
Flexible(
child: SizedBox(
width: _kCardFixedWidth,
child: Card(
child: Column(
children: [
header(),
body(),
],
).marginOnly(bottom: 10),
).marginOnly(left: _kCardLeftMargin, top: 15),
),
),
],
);
}
Widget header() {
return Row(
children: [
headerNameVersion(),
headerInstallEnable(),
],
).marginOnly(
left: _kContentHMargin,
top: 10,
bottom: 10,
right: _kContentHMargin,
);
}
Widget headerNameVersion() {
return Expanded(
child: Row(
children: [
Text(
widget.plugin.meta.name,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: _kTitleFontSize,
),
),
SizedBox(
width: 5,
),
Text(
plugin.meta.version,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: _kVersionFontSize,
),
)
],
),
);
}
Widget headerButton(String label, VoidCallback onPressed) {
return Container(
child: ElevatedButton(
onPressed: onPressed,
child: Text(translate(label)),
),
);
}
Widget headerInstallEnable() {
final installButton = headerButton(
installed ? 'Uninstall' : 'Install',
() {
bind.pluginInstall(
id: plugin.meta.id,
b: !installed,
);
},
);
if (installed) {
final updateButton = plugin.needUpdate
? headerButton('Update', () {
bind.pluginInstall(
id: plugin.meta.id,
b: !installed,
);
})
: Container();
final enableButton = !installed
? Container()
: headerButton(isEnabled ? 'Disable' : 'Enable', () {
if (isEnabled) {
clearPlugin(plugin.meta.id);
}
bind.pluginEnable(id: plugin.meta.id, v: !isEnabled);
setState(() {});
});
return Row(
children: [
updateButton,
SizedBox(
width: 10,
),
installButton,
SizedBox(
width: 10,
),
enableButton,
],
);
} else {
return installButton;
}
}
Widget body() {
return Column(children: [
author(),
description(),
more(),
]).marginOnly(
left: _kCardLeftMargin,
top: 4,
right: _kContentHMargin,
);
}
Widget author() {
return Align(
alignment: Alignment.centerLeft,
child: Text(plugin.meta.author),
);
}
Widget description() {
return Align(
alignment: Alignment.centerLeft,
child: Text(plugin.meta.description),
);
}
Widget more() {
if (!(installed && isEnabled)) {
return Container();
}
final List<Widget> children = [];
final model = getPluginModel(kLocationHostMainPlugin, plugin.meta.id);
if (model != null) {
children.add(PluginItem(
pluginId: plugin.meta.id,
peerId: '',
location: kLocationHostMainPlugin,
pluginModel: model,
isMenu: false,
));
}
return ExpansionTile(
title: Text('Options'),
controlAffinity: ListTileControlAffinity.leading,
children: children,
);
}
}

View File

@@ -762,6 +762,10 @@ class RustdeskImpl {
throw UnimplementedError("mainGetError");
}
bool mainShowOption({required String key, dynamic hint}) {
throw UnimplementedError("mainShowOption");
}
Future<void> mainSetOption(
{required String key, required String value, dynamic hint}) {
js.context.callMethod('setByName', [
@@ -1446,6 +1450,31 @@ class RustdeskImpl {
required int ptr,
dynamic hint}) {}
void sessionUnregisterPixelbufferTexture(
{required UuidValue sessionId,
required int display,
required int ptr,
dynamic hint}) {}
void sessionUnregisterGpuTexture(
{required UuidValue sessionId,
required int display,
required int ptr,
dynamic hint}) {}
void sessionSetRenderVisible(
{required UuidValue sessionId, required bool visible, dynamic hint}) {}
bool mainTextureRenderProbeSupported({dynamic hint}) {
return false;
}
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
return 0;
}
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
return Future(() =>
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
@@ -1640,6 +1669,78 @@ class RustdeskImpl {
throw UnimplementedError("sendUrlScheme");
}
Future<void> pluginEvent(
{required String id,
required String peer,
required Uint8List event,
dynamic hint}) {
throw UnimplementedError("pluginEvent");
}
Stream<EventToUI> pluginRegisterEventStream(
{required String id, dynamic hint}) {
throw UnimplementedError("pluginRegisterEventStream");
}
String? pluginGetSessionOption(
{required String id,
required String peer,
required String key,
dynamic hint}) {
throw UnimplementedError("pluginGetSessionOption");
}
Future<void> pluginSetSessionOption(
{required String id,
required String peer,
required String key,
required String value,
dynamic hint}) {
throw UnimplementedError("pluginSetSessionOption");
}
String? pluginGetSharedOption(
{required String id, required String key, dynamic hint}) {
throw UnimplementedError("pluginGetSharedOption");
}
Future<void> pluginSetSharedOption(
{required String id,
required String key,
required String value,
dynamic hint}) {
throw UnimplementedError("pluginSetSharedOption");
}
Future<void> pluginReload({required String id, dynamic hint}) {
throw UnimplementedError("pluginReload");
}
void pluginEnable({required String id, required bool v, dynamic hint}) {
throw UnimplementedError("pluginEnable");
}
bool pluginIsEnabled({required String id, dynamic hint}) {
throw UnimplementedError("pluginIsEnabled");
}
bool pluginFeatureIsEnabled({dynamic hint}) {
throw UnimplementedError("pluginFeatureIsEnabled");
}
Future<void> pluginSyncUi({required String syncTo, dynamic hint}) {
throw UnimplementedError("pluginSyncUi");
}
Future<void> pluginListReload({dynamic hint}) {
throw UnimplementedError("pluginListReload");
}
Future<void> pluginInstall(
{required String id, required bool b, dynamic hint}) {
throw UnimplementedError("pluginInstall");
}
bool isSupportMultiUiSession({required String version, dynamic hint}) {
return versionToNumber(v: version) > versionToNumber(v: '1.2.4');
}

View File

@@ -1,4 +1,5 @@
import 'dart:js' as js;
import 'dart:html' as html;
// cycle imports, maybe we can improve this
import 'package:flutter_hbb/consts.dart';
@@ -12,6 +13,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;

View File

@@ -0,0 +1,14 @@
abstract class NativeHandler {
bool onEvent(Map<String, dynamic> evt);
}
class NativeUiHandler extends NativeHandler {
NativeUiHandler._();
static NativeUiHandler instance = NativeUiHandler._();
@override
bool onEvent(Map<String, dynamic> evt) {
throw UnimplementedError();
}
}

View File

@@ -36,28 +36,8 @@ class RelativeMouseState {
}
class MainFlutterWindow: NSWindow {
private static let fullscreenWorkAreaSizes = NSMapTable<NSWindow, NSValue>(
keyOptions: [.weakMemory, .objectPointerPersonality],
valueOptions: .strongMemory
)
private static let fullscreenObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willEnterFullScreenNotification,
object: nil,
queue: .main
) { notification in
guard let window = notification.object as? NSWindow,
let screen = window.screen else {
return
}
fullscreenWorkAreaSizes.setObject(
NSValue(size: screen.visibleFrame.size),
forKey: window
)
}
override func awakeFromNib() {
rustdesk_core_main();
_ = MainFlutterWindow.fullscreenObserver
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
@@ -298,16 +278,6 @@ class MainFlutterWindow: NSWindow {
self.disableNativeRelativeMouseMode()
result(true)
case "getMacOSWorkAreaSize":
guard Thread.isMainThread,
let window = registrar.view?.window,
let size = MainFlutterWindow.fullscreenWorkAreaSizes
.object(forKey: window)?.sizeValue else {
result(nil)
break
}
result([Double(size.width), Double(size.height)])
default:
result(FlutterMethodNotImplemented)
}

View File

@@ -529,18 +529,17 @@ packages:
flutter_custom_cursor:
dependency: "direct main"
description:
path: "."
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
resolved-ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
url: "https://github.com/rustdesk-org/flutter_custom_cursor"
source: git
version: "0.0.3"
name: flutter_custom_cursor
sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
url: "https://pub.dev"
source: hosted
version: "0.0.4"
flutter_gpu_texture_renderer:
dependency: "direct main"
description:
path: "."
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
resolved-ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git
version: "0.0.1"
@@ -1299,8 +1298,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
source: git
version: "0.0.16"
@@ -1598,9 +1597,9 @@ packages:
dependency: "direct main"
description:
path: "plugins/window_size"
ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
resolved-ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
url: "https://github.com/21pages/flutter-desktop-embedding.git"
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
url: "https://github.com/google/flutter-desktop-embedding.git"
source: git
version: "0.1.0"
xdg_directories:

View File

@@ -58,15 +58,12 @@ dependencies:
git:
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
freezed_annotation: ^2.0.3
flutter_custom_cursor:
git:
url: https://github.com/rustdesk-org/flutter_custom_cursor
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
flutter_custom_cursor: ^0.0.4
window_size:
git:
url: https://github.com/21pages/flutter-desktop-embedding.git
url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: 51e67ce047c72b26810b99e8473ddb44612fe356
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
get: ^4.6.5
visibility_detector: ^0.4.0+2
contextmenu: ^3.0.0
@@ -91,13 +88,13 @@ dependencies:
texture_rgba_renderer:
git:
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
ref: 42797e0f03141dc2b585f76c64a13974508058b4
ref: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
percent_indicator: ^4.2.2
dropdown_button2: ^2.0.0
flutter_gpu_texture_renderer:
git:
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87
ref: 208619e750a5fd904c689a9babd6ccf0f7c1ca88
uuid: ^3.0.7
auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0

View File

@@ -1,35 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xterm/xterm.dart';
const _terminalSize = Size(400, 120);
Widget _terminalHarness(
Terminal terminal,
TerminalController controller,
) =>
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: _terminalSize.width,
height: _terminalSize.height,
child: TerminalMouseInteraction(
terminal,
controller: controller,
),
),
),
);
void _writeLines(Terminal terminal, int count) => terminal.write(
List.generate(count, (index) => 'line $index\r\n').join(),
);
void main() {
late Terminal terminal;
late List<String> output;
@@ -40,34 +12,6 @@ void main() {
..onOutput = output.add;
});
testWidgets('Linux Ctrl+V is not paste', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
try {
final messenger = tester.binding.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(
SystemChannels.platform,
(_) async => {'text': 'clipboard'},
);
final controller = TerminalController();
addTearDown(controller.dispose);
await tester.pumpWidget(_terminalHarness(terminal, controller));
await tester.tap(find.byType(TerminalView));
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
final controlVOutput = List.of(output);
output.clear();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(controlVOutput, ['\x16']);
expect(output, ['clipboard']);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
String? report(
TerminalMouseButton button, [
TerminalMouseButtonState state = TerminalMouseButtonState.down,
@@ -167,133 +111,4 @@ void main() {
isNull,
);
});
testWidgets('dragging below scrolls and extends selection', (tester) async {
final controller = TerminalController();
_writeLines(terminal, 80);
await tester.pumpWidget(_terminalHarness(terminal, controller));
final terminalView =
tester.state<TerminalViewState>(find.byType(TerminalView));
final scrollController = terminalView.widget.scrollController!;
scrollController.jumpTo(0);
await tester.pump();
final renderTerminal = terminalView.renderTerminal;
const localStart = Offset(20, 20);
final startCell = renderTerminal.getCellOffset(localStart);
final mouse = TestPointer(1, PointerDeviceKind.mouse);
final outside = Offset(20, renderTerminal.size.height);
await tester.handlePointerEventRecord([
PointerEventRecord(Duration.zero, [
mouse.down(renderTerminal.localToGlobal(localStart)),
mouse.move(renderTerminal.localToGlobal(outside)),
]),
PointerEventRecord(const Duration(milliseconds: 150), [
mouse.move(
renderTerminal.localToGlobal(outside + const Offset(1, 1)),
),
mouse.up(),
]),
]);
expect(scrollController.offset, greaterThan(0));
expect(controller.selection!.begin, startCell);
expect(controller.selection!.end.y, greaterThan(startCell.y));
final releasedOffset = scrollController.offset;
await tester.pump(const Duration(milliseconds: 100));
expect(scrollController.offset, releasedOffset);
});
testWidgets('tmux mouse input is reported without local selection',
(tester) async {
final controller = TerminalController();
terminal.write('\x1b[?1049h\x1b[?1002h\x1b[?1006hword');
await tester.pumpWidget(_terminalHarness(terminal, controller));
final renderTerminal = tester
.state<TerminalViewState>(find.byType(TerminalView))
.renderTerminal;
const wheel = Offset(120, 40);
await tester.sendEventToBinding(
PointerScrollEvent(
position: renderTerminal.localToGlobal(wheel),
scrollDelta: const Offset(0, 40),
),
);
await tester.pump();
final wheelCell = renderTerminal.getCellOffset(wheel);
expect(output.first, '\x1b[<65;${wheelCell.x + 1};${wheelCell.y + 1}M');
output.clear();
final clickPosition =
renderTerminal.getOffset(const CellOffset(0, 0)) + const Offset(1, 1);
final clickCell = renderTerminal.getCellOffset(clickPosition);
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
await mouse.down(renderTerminal.localToGlobal(clickPosition));
await mouse.up();
await tester.pump();
await mouse.down(renderTerminal.localToGlobal(clickPosition));
await mouse.up();
await tester.pump(kDoubleTapTimeout);
expect(output, [
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
]);
expect(controller.selection, isNull);
expect(controller.suspendedPointerInputs, isFalse);
output.clear();
const start = Offset(40, 40);
const end = Offset(240, 80);
final startCell = renderTerminal.getCellOffset(start);
final endCell = renderTerminal.getCellOffset(end);
await mouse.down(renderTerminal.localToGlobal(start));
await mouse.moveTo(renderTerminal.localToGlobal(end));
await tester.pump();
expect(output, [
'\x1b[<0;${startCell.x + 1};${startCell.y + 1}M',
'\x1b[<32;${endCell.x + 1};${endCell.y + 1}M',
]);
expect(controller.selection, isNull);
await mouse.up();
expect(output.last, '\x1b[<0;${endCell.x + 1};${endCell.y + 1}m');
expect(controller.suspendedPointerInputs, isFalse);
});
testWidgets('tmux drag stays suppressed after mouse mode is disabled',
(tester) async {
final controller = TerminalController();
terminal.write('\x1b[?1049h\x1b[?1002h\x1b[?1006hword');
await tester.pumpWidget(_terminalHarness(terminal, controller));
final renderTerminal = tester
.state<TerminalViewState>(find.byType(TerminalView))
.renderTerminal;
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
final start = renderTerminal.localToGlobal(const Offset(40, 40));
final end = renderTerminal.localToGlobal(const Offset(240, 80));
await mouse.down(start);
output.clear();
terminal.write('\x1b[?1002l');
await mouse.moveTo(end);
expect(output, isEmpty);
expect(controller.selection, isNull);
expect(controller.suspendedPointerInputs, isTrue);
terminal.write('\x1b[?1002h');
await mouse.moveTo(start);
expect(output, isEmpty);
expect(controller.selection, isNull);
await mouse.up();
expect(output, isEmpty);
expect(controller.suspendedPointerInputs, isFalse);
await mouse.down(start);
output.clear();
terminal.write('\x1b[?1002l');
await mouse.up();
await tester.pump(kDoubleTapTimeout);
expect(output, isEmpty);
expect(controller.selection, isNull);
expect(controller.suspendedPointerInputs, isFalse);
});
}

View File

@@ -42,13 +42,6 @@ impl Enigo {
&mut self.custom_mouse
}
/// Override the display server guessed in `Default::default`: on "x11" every method here
/// routes to `xdo`, and a null xdo context makes all of them silent no-ops. A caller
/// installing custom devices knows better than the guess.
pub fn set_is_x11(&mut self, is_x11: bool) {
self.is_x11 = is_x11;
}
/// Clear remapped keycodes
pub fn tfc_clear_remapped(&mut self) {
if let Some(tfc) = &mut self.tfc {
@@ -397,52 +390,3 @@ fn test_key_seq() {
let mut en = Enigo::new();
en.key_sequence("^^");
}
/// Both directions: the failure is silent, so a one-directional test passes against the bug.
#[test]
fn test_custom_mouse_dispatch_follows_is_x11() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct CountingMouse(Arc<AtomicUsize>);
impl MouseControllable for CountingMouse {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn mouse_move_to(&mut self, _x: i32, _y: i32) {
self.0.fetch_add(1, Ordering::Relaxed);
}
fn mouse_move_relative(&mut self, _x: i32, _y: i32) {}
fn mouse_down(&mut self, _button: MouseButton) -> crate::ResultType {
Ok(())
}
fn mouse_up(&mut self, _button: MouseButton) {}
fn mouse_click(&mut self, _button: MouseButton) {}
fn mouse_scroll_x(&mut self, _length: i32) {}
fn mouse_scroll_y(&mut self, _length: i32) {}
}
let calls = Arc::new(AtomicUsize::new(0));
let mut en = Enigo::new();
en.set_custom_mouse(Box::new(CountingMouse(calls.clone())));
en.set_is_x11(false);
en.mouse_move_to(10, 20);
assert_eq!(
calls.load(Ordering::Relaxed),
1,
"custom mouse was not reached on the non-x11 branch"
);
// Negative control: on the x11 branch the custom device must be bypassed entirely.
en.set_is_x11(true);
en.mouse_move_to(30, 40);
assert_eq!(
calls.load(Ordering::Relaxed),
1,
"custom mouse was reached on the x11 branch"
);
}

View File

@@ -14,13 +14,13 @@ wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "
# `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`)
# and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is
# preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is pinned by
# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.4). We deliberately do
# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.2). We deliberately do
# NOT depend on the `libdrmtap-sys` crate: its build.rs statically compiles the whole libdrmtap C tree
# and a CAP_SYS_ADMIN helper and emits `-ldrm -lseccomp -lcap`, which would defeat the dlopen model.
# Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of
# common/mod.rs, so `scrap/drm` on its own would compile nothing. The root crate happens to always
# enable `scrap/wayland`, which is what hid this.
drm = ["wayland", "hbb_common/wayland_probe"]
drm = ["wayland"]
mediacodec = ["ndk"]
linux-pkg-config = ["dep:pkg-config"]
hwcodec = ["dep:hwcodec"]

View File

@@ -48,7 +48,6 @@ pub struct Capturer {
duplication: ComPtr<IDXGIOutputDuplication>,
fastlane: bool,
surface: ComPtr<IDXGISurface>,
readable: ComPtr<ID3D11Texture2D>,
texture: ComPtr<ID3D11Texture2D>,
width: usize,
height: usize,
@@ -164,7 +163,6 @@ impl Capturer {
duplication: ComPtr(duplication),
fastlane: desc.DesktopImageInSystemMemory == TRUE,
surface: ComPtr(ptr::null_mut()),
readable: ComPtr(ptr::null_mut()),
texture: ComPtr(ptr::null_mut()),
width: display.width() as usize,
height: display.height() as usize,
@@ -348,14 +346,14 @@ impl Capturer {
if self.fastlane {
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
} else {
self.ohgodwhat(frame.0)?;
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
}
Ok((rect.pBits, rect.Pitch))
}
// copy from GPU memory to system memory
unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<()> {
unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> {
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
(*frame).QueryInterface(
&IID_ID3D11Texture2D,
@@ -372,37 +370,24 @@ impl Capturer {
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
texture_desc.MiscFlags = 0;
// A staging texture per frame costs one kernel paged pool allocation per frame.
let mut current: D3D11_TEXTURE2D_DESC = mem::zeroed();
if !self.surface.is_null() {
(*self.readable.0).GetDesc(&mut current);
}
if current.Width != texture_desc.Width
|| current.Height != texture_desc.Height
|| current.Format != texture_desc.Format
{
let mut readable = ptr::null_mut();
wrap_hresult((*self.device.0).CreateTexture2D(
&mut texture_desc,
ptr::null(),
&mut readable,
))?;
(*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM);
let readable = ComPtr(readable);
let mut readable = ptr::null_mut();
wrap_hresult((*self.device.0).CreateTexture2D(
&mut texture_desc,
ptr::null(),
&mut readable,
))?;
(*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM);
let readable = ComPtr(readable);
let mut surface = ptr::null_mut();
(*readable.0).QueryInterface(
&IID_IDXGISurface,
&mut surface as *mut *mut _ as *mut *mut _,
);
let mut surface = ptr::null_mut();
(*readable.0).QueryInterface(
&IID_IDXGISurface,
&mut surface as *mut *mut _ as *mut *mut _,
);
self.readable = readable;
self.surface = ComPtr(surface);
}
(*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _);
(*self.context.0).CopyResource(self.readable.0 as *mut _, texture.0 as *mut _);
Ok(())
Ok(surface)
}
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {

View File

@@ -19,18 +19,6 @@ static MISSING_LOGICAL_SIZE_WARNED: std::sync::atomic::AtomicBool =
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
// drm builds only: an unnamed-endpoint failure there forks the probe child, and the pollers
// turn every few hundred milliseconds. Every other failure is one cheap in-process error.
#[cfg(any(test, feature = "drm"))]
const FAILED_LOOKUP_BACKOFF: Duration = Duration::from_secs(5);
#[cfg(any(test, feature = "drm"))]
static LAST_FAILED_LOOKUP: Mutex<Option<Instant>> = Mutex::new(None);
#[cfg(feature = "drm")]
static LOOKUP_FAILURE_WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub struct Displays {
pub primary: usize,
pub displays: Vec<WaylandDisplayInfo>,
@@ -183,76 +171,11 @@ fn get_primary_monitor() -> Option<String> {
.or_else(try_gdbus_primary)
}
// Pure, so the backoff policy is testable without a compositor.
#[cfg(any(test, feature = "drm"))]
fn lookup_allowed(failed_at: Option<Instant>, now: Instant) -> bool {
failed_at.map_or(true, |at| {
now.saturating_duration_since(at) >= FAILED_LOOKUP_BACKOFF
})
}
#[cfg(feature = "drm")]
fn backed_off() -> bool {
let failed_at = *LAST_FAILED_LOOKUP.lock().unwrap();
!lookup_allowed(failed_at, Instant::now())
}
// Mirrors the probe module's gate, latch included: connecting consumes WAYLAND_SOCKET, so a
// once-named endpoint must stay named for the life of the process.
#[cfg(feature = "drm")]
fn endpoint_named() -> bool {
use std::sync::atomic::{AtomicBool, Ordering};
static WAS_NAMED: AtomicBool = AtomicBool::new(false);
let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"]
.iter()
.any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty()));
if named {
WAS_NAMED.store(true, Ordering::Release);
}
WAS_NAMED.load(Ordering::Acquire)
}
// Enumerates and keeps the failure stamp current. Suppresses nothing itself: one-shot callers
// (session init, pipewire) must always get a fresh read, or a transient failure latches.
fn enumerate_displays() -> hbb_common::ResultType<Vec<WaylandDisplayInfo>> {
// Read before connecting, which consumes WAYLAND_SOCKET.
#[cfg(feature = "drm")]
let named = endpoint_named();
let probed = get_wayland_displays();
// Only the failure that would fork stamps; a named endpoint fails cheaply in-process.
#[cfg(feature = "drm")]
{
*LAST_FAILED_LOOKUP.lock().unwrap() = (probed.is_err() && !named).then(Instant::now);
if let Err(err) = &probed {
if !LOOKUP_FAILURE_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
warn!("Failed to get wayland displays: {}", err);
}
} else {
LOOKUP_FAILURE_WARNED.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
probed
}
// True when a lookup now could neither hit the cache nor probe. Pollers skip their turn on
// it and keep their last published state; one-shot callers must not consult it.
#[cfg(feature = "drm")]
pub fn wayland_lookup_suppressed() -> bool {
DISPLAYS.lock().unwrap().is_none() && backed_off()
}
// Whether any failure stamp exists, expired or not: pollers use it to tell a first failure
// from one that has already persisted across a backoff.
#[cfg(feature = "drm")]
pub fn wayland_failure_stamped() -> bool {
LAST_FAILED_LOOKUP.lock().unwrap().is_some()
}
pub fn get_displays() -> Arc<Displays> {
let mut lock = DISPLAYS.lock().unwrap();
match lock.as_ref() {
Some(displays) => displays.clone(),
None => match enumerate_displays() {
None => match get_wayland_displays() {
Ok(displays) => {
let mut primary_index = None;
if let Some(name) = get_primary_monitor() {
@@ -278,9 +201,8 @@ pub fn get_displays() -> Arc<Displays> {
*lock = Some(displays.clone());
displays
}
Err(_err) => {
#[cfg(not(feature = "drm"))]
warn!("Failed to get wayland displays: {}", _err);
Err(err) => {
warn!("Failed to get wayland displays: {}", err);
Arc::new(Displays {
primary: 0,
displays: Vec::new(),
@@ -293,8 +215,6 @@ pub fn get_displays() -> Arc<Displays> {
#[inline]
pub fn clear_wayland_displays_cache() {
let _ = DISPLAYS.lock().unwrap().take();
// The failure stamp survives on purpose: it describes the seat, not the cache, and the
// capturer rebuild loop clears about once a second.
}
// Return (min_x, max_x, min_y, max_y)
@@ -303,21 +223,17 @@ pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
desktop_rect_of(&wayland_displays.displays)
}
// The desktop rect and per-display logical rects, read live from the compositor in a single
// roundtrip (drm builds may skip a turn during the failure backoff). Skips the displays cache
// and the primary-monitor detection, cheap enough to poll. rustdesk/rustdesk#15601
// The desktop rect and per-display logical rects, always read live from the
// compositor in a single roundtrip. Skips the displays cache and the primary-monitor
// detection (which may spawn external commands), so it is cheap enough to poll for
// layout changes. https://github.com/rustdesk/rustdesk/issues/15601
pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<DisplayRect>)> {
#[cfg(feature = "drm")]
if backed_off() {
return None;
}
match enumerate_displays() {
match get_wayland_displays() {
Ok(displays) => {
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
}
Err(_err) => {
#[cfg(not(feature = "drm"))]
warn!("Failed to get wayland displays: {}", _err);
Err(err) => {
warn!("Failed to get wayland displays: {}", err);
None
}
}
@@ -470,40 +386,6 @@ fn map_axis(v: i32, base_origin: i32, base_extent: i32, live_origin: i32, live_e
mod tests {
use super::*;
#[test]
fn test_lookup_backoff_boundaries() {
// Future `now`s sidestep Instant subtraction, which can panic near boot.
let failed_at = Instant::now();
assert!(lookup_allowed(None, failed_at));
assert!(!lookup_allowed(
Some(failed_at),
failed_at + FAILED_LOOKUP_BACKOFF / 2
));
assert!(lookup_allowed(
Some(failed_at),
failed_at + FAILED_LOOKUP_BACKOFF
));
}
#[test]
fn test_lookup_stamp_from_the_future_only_waits() {
// saturating_duration_since answers zero rather than underflowing.
let now = Instant::now();
assert!(!lookup_allowed(Some(now + FAILED_LOOKUP_BACKOFF), now));
}
#[test]
fn test_clear_keeps_the_failure_stamp() {
// The stamp describes the seat, not the cache: the ~1/s capturer rebuild loop clears,
// and dropping the stamp with it would defeat the backoff. Sole test touching these
// statics; serialize before adding another.
*LAST_FAILED_LOOKUP.lock().unwrap() = Some(Instant::now());
clear_wayland_displays_cache();
let stamp = *LAST_FAILED_LOOKUP.lock().unwrap();
assert!(stamp.is_some());
*LAST_FAILED_LOOKUP.lock().unwrap() = None;
}
fn display(
x: i32,
y: i32,

View File

@@ -7,7 +7,7 @@ arch=('x86_64')
url=""
license=('AGPL-3.0')
groups=()
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'gst-plugins-base' 'gst-plugin-pipewire')
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
makedepends=()
checkdepends=()
optdepends=()

View File

@@ -5,23 +5,6 @@
<Fragment>
<!-- Regs for shortcuts are defined in "Fragments/ShortcutProperties.wxs" -->
<!-- Component that persists the property values to the registry so they are available during an upgrade/modify -->
<Property Id="CURRENT_SHARE_RDP" Secure="yes">
<RegistrySearch Id="CurrentShareRdpSearch" Root="HKLM" Key="Software\$(var.Product)\InstallState\$(var.Product)" Name="share_rdp" Type="raw" />
</Property>
<Property Id="LEGACY_SHARE_RDP" Secure="yes">
<RegistrySearch Id="LegacyShareRdpSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="share_rdp" Type="raw" Bitness="always64" />
</Property>
<Property Id="LEGACY_SHARE_RDP32" Secure="yes">
<RegistrySearch Id="LegacyShareRdp32Search" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="share_rdp" Type="raw" Bitness="always32" />
</Property>
<Property Id="APP_WINDOWS_INSTALLER32" Secure="yes">
<RegistrySearch Id="AppWindowsInstaller32Search" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" Bitness="always32" />
</Property>
<Property Id="SHARE_RDP" Secure="yes" />
<SetProperty Action="SetShareRdpFromCurrentState" Id="SHARE_RDP" Value="[CURRENT_SHARE_RDP]" After="AppSearch" Sequence="first" Condition="CURRENT_SHARE_RDP" />
<SetProperty Action="SetShareRdpFromLegacyState" Id="SHARE_RDP" Value="[LEGACY_SHARE_RDP]" After="SetShareRdpFromCurrentState" Sequence="first" Condition="NOT CURRENT_SHARE_RDP AND LEGACY_SHARE_RDP" />
<SetProperty Action="SetShareRdpFromLegacyState32" Id="SHARE_RDP" Value="[LEGACY_SHARE_RDP32]" After="SetShareRdpFromLegacyState" Sequence="first" Condition="NOT CURRENT_SHARE_RDP AND NOT LEGACY_SHARE_RDP AND LEGACY_SHARE_RDP32" />
<DirectoryRef Id="INSTALLFOLDER_INNER">
<Component Id="Product.Registry.InstallFolder" Guid="3196EDA7-9AEF-4705-A0C8-E3F3ECCCB153">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
@@ -57,29 +40,17 @@
</RegistryKey>
</Component>
<Component Id="Product.Registry.InstallState" Guid="E310003A-C292-4851-AE20-7A9B186060CE">
<RegistryKey Root="HKLM" Key="Software\$(var.Product)\InstallState\$(var.Product)" ForceDeleteOnUninstall="yes">
<!--For compatibility with registry values from previous versions-->
<Component Id="Product.Registry.UninstallApp" Guid="FC1A3D2E-5642-FBD8-CFA6-5ECAC6DE69A8">
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" >
<RegistryValue Type="string" Name="BuildDate" Value="$(var.BuildDate)" />
<RegistryValue Type="string" Name="share_rdp" Value="[SHARE_RDP]" />
<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />
<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />
<RegistryValue Type="string" Name="MsiProductCode" Value="[ProductCode]" KeyPath="yes" />
<!--$InstallStateStart$-->
<!--$InstallStateEnd$-->
<RegistryValue Type="string" Name="share_rdp" Value="" />
<!--$ArpStart$-->
<!--$ArpEnd$-->
</RegistryKey>
</Component>
</DirectoryRef>
<StandardDirectory Id="CommonAppDataFolder">
<Component Id="Product.Registry.RemoveLegacyUninstall64" Guid="7524B741-94C2-4C50-A151-DDB37860DC20" Bitness="always64" KeyPath="yes" Condition="APP_WINDOWS_INSTALLER=&quot;#1&quot;">
<RemoveRegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Action="removeOnInstall" />
</Component>
<Component Id="Product.Registry.RemoveLegacyUninstall32" Guid="CB50B9AA-B83A-4AC2-BB27-81358F0E74E6" Bitness="always32" KeyPath="yes" Condition="APP_WINDOWS_INSTALLER32=&quot;#1&quot;">
<RemoveRegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Action="removeOnInstall" />
</Component>
</StandardDirectory>
</Fragment>
</Wix>

View File

@@ -27,12 +27,10 @@
<!--$ArpStart$-->
<!--$ArpEnd$-->
<SetProperty Action="SetArpInstallLocation" Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER_INNER]" After="CostFinalize" Sequence="execute" />
<!--$CustomClientPropsStart$-->
<!--$CustomClientPropsEnd$-->
<Property Id="APP_WINDOWS_INSTALLER" Secure="yes">
<Property Id="APP_WINDOWS_INSTALLER">
<RegistrySearch Id="AppWindowsInstallerFolderSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" />
</Property>
</Fragment>

View File

@@ -21,6 +21,8 @@ This file contains the declaration of all the localizable strings.
<!-- Client related strings -->
<String Id="F_Client" Value="Client" />
<String Id="F_Client_Desc" Value="The user interface. Plays media files." />
<String Id="F_Client_Plugins" Value="Plugins" />
<String Id="F_Client_Plugins_Desc" Value="Plugins for the client." />
<String Id="F_LAVFilters" Value="LAV Filters" />
<String Id="F_LAVFilters_Desc" Value="Recommended directshow filters for best audio and video playback experience." />
@@ -33,6 +35,8 @@ This file contains the declaration of all the localizable strings.
<!-- Server related strings -->
<String Id="F_Server" Value="Server" />
<String Id="F_Server_Desc" Value="The server part of RustDesk. Provides the MediaLibrary and other services." />
<String Id="F_Server_Plugins" Value="Plugins" />
<String Id="F_Server_Plugins_Desc" Value="Plugins for the server." />
<String Id="Service_DisplayName" Value="RustDesk Service" />
<String Id="Service_Description" Value="This service runs the RustDesk Server." />

View File

@@ -14,7 +14,6 @@
<Media Id="1" Cabinet="cab1.cab" EmbedCab="yes" CompressionLevel="high" />
<Icon Id="AppIcon" SourceFile="Resources\icon.ico" />
<CustomAction Id="BlockSelfInstalledApp" Error="!(loc.AnotherAppDialogDescription)" />
<!-- User Interface -->
<WixVariable Id="WixUILicenseRtf" Value="License.rtf" />
@@ -23,11 +22,10 @@
<UIRef Id="WixUI_ErrorProgressText" />
<InstallUISequence>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER=&quot;#0&quot; OR APP_WINDOWS_INSTALLER32=&quot;#0&quot;)"/>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND APP_WINDOWS_INSTALLER=&quot;#0&quot;"/>
</InstallUISequence>
<InstallExecuteSequence>
<Custom Action="BlockSelfInstalledApp" After="AppSearch" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER=&quot;#0&quot; OR APP_WINDOWS_INSTALLER32=&quot;#0&quot;)" />
<InstallExecute After="RemoveExistingProducts" />
<!--Only do InstallValidate if is not Uninstall-->
@@ -47,9 +45,7 @@
<ComponentRef Id="Product.Registry.CommandPlay" />
<ComponentRef Id="Product.Registry.URLProtocol" />
<ComponentRef Id="Product.Registry.Command" />
<ComponentRef Id="Product.Registry.InstallState" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall64" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall32" />
<ComponentRef Id="Product.Registry.UninstallApp" />
<ComponentRef Id="App.StartMenu" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" />

View File

@@ -12,7 +12,6 @@ import platform
from pathlib import Path
from itertools import chain
import shutil
from xml.sax.saxutils import quoteattr
g_indent_unit = "\t"
g_version = ""
@@ -55,14 +54,14 @@ def make_parser():
parser.add_argument(
"--arp",
action="store_true",
help="Deprecated; native MSI ARP registration is always used.",
help="Is ARPSYSTEMCOMPONENT",
default=False,
)
parser.add_argument(
"--custom-arp",
type=str,
default="{}",
help='Custom arp properties, e.g. \'{"Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}}\'',
help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'',
)
parser.add_argument(
"-c", "--custom", action="store_true", help="Is custom client", default=False
@@ -259,19 +258,25 @@ def gen_custom_dialog_bitmaps():
)
def gen_native_arp_properties():
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
def func(lines, index_start):
indent = g_indent_unit * 2
lines_new = []
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/arpsystemcomponent?redirectedfrom=MSDN-->\n"
)
lines_new.append(
f'{indent}<!--<Property Id="ARPSYSTEMCOMPONENT" Value="1" />-->\n\n'
)
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
for _, v in g_arpsystemcomponent.items():
if "msi" in v and "v" in v:
lines_new.append(
f'{indent}<Property Id={quoteattr(str(v["msi"]))} '
f'Value={quoteattr(str(v["v"]))} />\n'
f'{indent}<Property Id="{v["msi"]}" Value="{v["v"]}" />\n'
)
for i, line in enumerate(lines_new):
@@ -286,16 +291,94 @@ def gen_native_arp_properties():
)
def gen_install_state_values():
def get_folder_size(folder_path):
total_size = 0
folder = Path(folder_path)
for file in folder.glob("**/*"):
if file.is_file():
total_size += file.stat().st_size
return total_size
def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
def func(lines, index_start):
indent = g_indent_unit * 5
lines_new = []
for name, value in g_arpsystemcomponent.items():
if "msi" not in value and "v" in value:
value_type = value.get("t", "string")
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayName" Value="{args.app_name}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayIcon" Value="[INSTALLFOLDER_INNER]{args.app_name}.exe" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayVersion" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Publisher" Value="{args.manufacturer}" />\n'
)
installDate = datetime.datetime.now().strftime("%Y%m%d")
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallDate" Value="{installDate}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallSource" Value="[InstallSource]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
)
# EstimatedSize in uninstall registry must be in KB.
estimated_size_bytes = get_folder_size(dist_dir)
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="ModifyPath" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Id="NoModify" Value="1" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="UninstallString" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="QuietUninstallString" Value="MsiExec.exe /qn /X [ProductCode]" />\n'
)
vs = g_version.split(".")
major, minor, build = vs[0], vs[1], vs[2]
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Version" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMajor" Value="{major}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMinor" Value="{minor}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionBuild" Value="{build}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />\n'
)
for k, v in g_arpsystemcomponent.items():
if "v" in v:
t = v["t"] if "t" in v is None else "string"
lines_new.append(
f'{indent}<RegistryValue Type={quoteattr(str(value_type))} '
f'Name={quoteattr(str(name))} Value={quoteattr(str(value["v"]))} />\n'
f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
)
for i, line in enumerate(lines_new):
@@ -304,35 +387,24 @@ def gen_install_state_values():
return gen_content_between_tags(
"Package/Components/Regs.wxs",
"<!--$InstallStateStart$-->",
"<!--$InstallStateEnd$-->",
"<!--$ArpStart$-->",
"<!--$ArpEnd$-->",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT(args, _dist_dir):
def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
try:
custom_arp = dict(json.loads(args.custom_arp))
except (json.JSONDecodeError, TypeError, ValueError) as e:
custom_arp = json.loads(args.custom_arp)
g_arpsystemcomponent.update(custom_arp)
except json.JSONDecodeError as e:
print(f"Failed to decode custom arp: {e}")
return False
if any(not isinstance(value, dict) for value in custom_arp.values()):
print("Custom arp entries must be objects.")
return False
if any(
isinstance(value, dict) and value.get("msi") == "ARPSYSTEMCOMPONENT"
for value in custom_arp.values()
):
print("ARPSYSTEMCOMPONENT is not allowed; native MSI ARP registration must remain visible.")
return False
g_arpsystemcomponent.update(custom_arp)
if not gen_native_arp_properties():
return False
return gen_install_state_values()
if args.arp:
return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
else:
return gen_custom_ARPSYSTEMCOMPONENT_False(args)
def gen_conn_type(args):
def func(lines, index_start):

View File

@@ -0,0 +1,5 @@
#%PAM-1.0
@include common-auth
@include common-account
@include common-session
@include common-password

5
res/pam.d/rustdesk.suse Normal file
View File

@@ -0,0 +1,5 @@
#%PAM-1.0
auth include common-auth
account include common-account
session include common-session
password include common-password

View File

@@ -5,7 +5,7 @@ Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1 xdotool
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)

View File

@@ -5,7 +5,7 @@ Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libXfixes alsa-lib libva gstreamer1-plugins-base
Requires: gtk3 libxcb libXfixes alsa-lib libva pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3 libxdo
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)

View File

@@ -3,7 +3,7 @@ Version: 1.1.9
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1 xdotool
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/

View File

@@ -5,7 +5,7 @@ Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libXfixes alsa-lib libva2 gstreamer1-plugins-base
Requires: gtk3 libxcb libXfixes alsa-lib libva2 pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3 libxdo
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/

130
res/startwm.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh.
#
# This script is an example. You might need to edit this script
# depending on your distro if it doesn't work for you.
#
# Uncomment the following line for debug:
# exec xterm
# Execution sequence for interactive login shell - pseudocode
#
# IF /etc/profile is readable THEN
# execute ~/.bash_profile
# END IF
# IF ~/.bash_profile is readable THEN
# execute ~/.bash_profile
# ELSE
# IF ~/.bash_login is readable THEN
# execute ~/.bash_login
# ELSE
# IF ~/.profile is readable THEN
# execute ~/.profile
# END IF
# END IF
# END IF
pre_start()
{
if [ -r /etc/profile ]; then
. /etc/profile
fi
if [ -r ~/.bash_profile ]; then
. ~/.bash_profile
else
if [ -r ~/.bash_login ]; then
. ~/.bash_login
else
if [ -r ~/.profile ]; then
. ~/.profile
fi
fi
fi
return 0
}
# When logging out from the interactive shell, the execution sequence is:
#
# IF ~/.bash_logout exists THEN
# execute ~/.bash_logout
# END IF
post_start()
{
if [ -r ~/.bash_logout ]; then
. ~/.bash_logout
fi
return 0
}
#start the window manager
wm_start()
{
if [ -r /etc/default/locale ]; then
. /etc/default/locale
export LANG LANGUAGE
fi
# debian
if [ -r /etc/X11/Xsession ]; then
pre_start
. /etc/X11/Xsession
post_start
exit 0
fi
# alpine
# Don't use /etc/X11/xinit/Xsession - it doesn't work
if [ -f /etc/alpine-release ]; then
if [ -f /etc/X11/xinit/xinitrc ]; then
pre_start
/etc/X11/xinit/xinitrc
post_start
else
echo "** xinit package isn't installed" >&2
exit 1
fi
fi
# el
if [ -r /etc/X11/xinit/Xsession ]; then
pre_start
. /etc/X11/xinit/Xsession
post_start
exit 0
fi
# suse
if [ -r /etc/X11/xdm/Xsession ]; then
# since the following script run a user login shell,
# do not execute the pseudo login shell scripts
. /etc/X11/xdm/Xsession
exit 0
elif [ -r /usr/etc/X11/xdm/Xsession ]; then
. /usr/etc/X11/xdm/Xsession
exit 0
fi
pre_start
xterm
post_start
}
#. /etc/environment
#export PATH=$PATH
#export LANG=$LANG
# change PATH to be what your environment needs usually what is in
# /etc/environment
#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
#export PATH=$PATH
# for PATH and LANG from /etc/environment
# pam will auto process the environment file if /etc/pam.d/xrdp-sesman
# includes
# auth required pam_env.so readenv=1
wm_start
exit 1

30
res/xorg.conf Normal file
View File

@@ -0,0 +1,30 @@
Section "Monitor"
Identifier "Dummy Monitor"
# Default HorizSync 31.50 - 48.00 kHz
HorizSync 5.0 - 150.0
# Default VertRefresh 50.00 - 70.00 Hz
VertRefresh 5.0 - 100.0
# Taken from https://www.xpra.org/xorg.conf
Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135
Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757
EndSection
Section "Device"
Identifier "Dummy VideoCard"
Driver "dummy"
# Default VideoRam 4096
# (1920 * 1080 * 4) / 1024 = 8100
VideoRam 8100
EndSection
Section "Screen"
Identifier "Dummy Screen"
Device "Dummy VideoCard"
Monitor "Dummy Monitor"
SubSectionSub "Display"
Depth 24
Modes "1920x1080" "1280x720"
EndSubSection
EndSection

View File

@@ -101,6 +101,18 @@ const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
pub const VIDEO_QUEUE_SIZE: usize = 120;
const MAX_DECODE_FAIL_COUNTER: usize = 3;
#[cfg(target_os = "linux")]
pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited";
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY: &str = "Desktop session not ready";
pub const LOGIN_MSG_DESKTOP_XSESSION_FAILED: &str = "Desktop xsession failed";
pub const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER: &str = "Desktop session another user login";
pub const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND: &str = "Desktop xorg not found";
// ls /usr/share/xsessions/
pub const LOGIN_MSG_DESKTOP_NO_DESKTOP: &str = "Desktop none";
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY: &str =
"Desktop session not ready, password empty";
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str =
"Desktop session not ready, password wrong";
pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password";
pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password";
pub const LOGIN_MSG_2FA_WRONG: &str = "Wrong 2FA Code";
@@ -2727,16 +2739,6 @@ impl LoginConfigHandler {
} else {
Bytes::new()
};
let os_login: MessageField<OSLogin> = if self.conn_type == ConnType::TERMINAL {
Some(OSLogin {
username: os_username,
password: os_password,
..Default::default()
})
.into()
} else {
Default::default()
};
let mut lr = LoginRequest {
username: pure_id,
password: password.into(),
@@ -2746,7 +2748,12 @@ impl LoginConfigHandler {
option: self.get_option_message(true).into(),
session_id: self.session_id,
version: crate::VERSION.to_string(),
os_login,
os_login: Some(OSLogin {
username: os_username,
password: os_password,
..Default::default()
})
.into(),
hwid,
avatar,
..Default::default()
@@ -3341,12 +3348,55 @@ struct LoginErrorMsgBox {
lazy_static::lazy_static! {
static ref LOGIN_ERROR_MAP: Arc<HashMap<&'static str, LoginErrorMsgBox>> = {
use config::LINK_HEADLESS_LINUX_SUPPORT;
let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{
msgtype: "error",
title: "Login Error",
text: "Login screen using Wayland is not supported",
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
try_again: true,
}), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{
msgtype: "session-login",
title: "",
text: "",
link: "",
try_again: true,
}), (LOGIN_MSG_DESKTOP_XSESSION_FAILED, LoginErrorMsgBox{
msgtype: "session-re-login",
title: "",
text: "",
link: "",
try_again: true,
}), (LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LoginErrorMsgBox{
msgtype: "info-nocancel",
title: "another_user_login_title_tip",
text: "another_user_login_text_tip",
link: "",
try_again: false,
}), (LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LoginErrorMsgBox{
msgtype: "info-nocancel",
title: "xorg_not_found_title_tip",
text: "xorg_not_found_text_tip",
link: LINK_HEADLESS_LINUX_SUPPORT,
try_again: true,
}), (LOGIN_MSG_DESKTOP_NO_DESKTOP, LoginErrorMsgBox{
msgtype: "info-nocancel",
title: "no_desktop_title_tip",
text: "no_desktop_text_tip",
link: LINK_HEADLESS_LINUX_SUPPORT,
try_again: true,
}), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, LoginErrorMsgBox{
msgtype: "session-login-password",
title: "",
text: "",
link: "",
try_again: true,
}), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, LoginErrorMsgBox{
msgtype: "session-login-re-password",
title: "",
text: "",
link: "",
try_again: true,
}), (LOGIN_MSG_NO_PASSWORD_ACCESS, LoginErrorMsgBox{
msgtype: "wait-remote-accept-nook",
title: "Prompt",
@@ -3615,7 +3665,17 @@ pub async fn handle_hash(
hasher.finalize()[..].into()
};
send_login(lc.clone(), String::new(), String::new(), password, peer).await;
let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL);
let (os_username, os_password) = if is_terminal {
("".to_owned(), "".to_owned())
} else {
(
lc.read().unwrap().get_option("os-username"),
lc.read().unwrap().get_option("os-password"),
)
};
send_login(lc.clone(), os_username, os_password, password, peer).await;
lc.write().unwrap().hash = hash;
true
}

View File

@@ -1437,6 +1437,14 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
crate::flutter::update_file_clipboard_required();
// on connection established client
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::plugin::handle_listen_event(
crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(),
self.handler.get_id(),
);
}
if self.handler.is_file_transfer() {
@@ -1980,6 +1988,26 @@ impl<T: InvokeUiSession> Remote<T> {
);
}
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::PluginRequest(p)) => {
allow_err!(crate::plugin::handle_server_event(
&p.id,
&self.handler.get_id(),
&p.content
));
// to-do: show message box on UI when error occurs?
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::PluginFailure(p)) => {
let name = if p.name.is_empty() {
"plugin".to_string()
} else {
p.name
};
self.handler.msgbox("custom-nocancel", &name, &p.msg, "");
}
Some(misc::Union::SupportedEncoding(e)) => {
log::info!("update supported encoding:{:?}", e);
self.handler.lc.write().unwrap().supported_encoding = e;
@@ -2260,8 +2288,12 @@ impl<T: InvokeUiSession> Remote<T> {
.msgbox("custom-error", "Privacy mode", "Peer denied", "");
self.update_privacy_mode(impl_key, false);
}
back_notification::PrivacyModeState::PrvOnFailedPlugin
| back_notification::PrivacyModeState::PrvOnFailed => {
back_notification::PrivacyModeState::PrvOnFailedPlugin => {
self.handler
.msgbox("custom-error", "Privacy mode", "Please install plugins", "");
self.update_privacy_mode(impl_key, false);
}
back_notification::PrivacyModeState::PrvOnFailed => {
self.handler.msgbox(
"custom-error",
"Privacy mode",

View File

@@ -105,7 +105,7 @@ lazy_static::lazy_static! {
// Is server logic running. The server code can invoked to run by the main process if --server is not running.
static ref SERVER_RUNNING: Arc<RwLock<bool>> = Default::default();
static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--"));
static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned());
static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned());
}
pub struct SimpleCallOnReturn {
@@ -122,8 +122,6 @@ impl Drop for SimpleCallOnReturn {
}
pub fn global_init() -> bool {
#[cfg(all(target_os = "linux", feature = "drm"))]
crate::platform::linux::dispatch_wayland_display_probe();
#[cfg(target_os = "linux")]
{
if !crate::platform::linux::is_x11() {
@@ -1087,15 +1085,8 @@ fn get_api_server_(api: String, custom: String) -> String {
#[inline]
pub fn is_public(url: &str) -> bool {
let parsed = url::Url::parse(url)
.ok()
.filter(|parsed| parsed.has_host())
.or_else(|| url::Url::parse(&format!("http://{url}")).ok());
let Some(host) = parsed.as_ref().and_then(url::Url::host_str) else {
return false;
};
let host = host.strip_suffix('.').unwrap_or(host);
host == "rustdesk.com" || host.ends_with(".rustdesk.com")
let url = url.to_ascii_lowercase();
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
}
pub fn get_udp_punch_enabled() -> bool {
@@ -2887,16 +2878,6 @@ mod tests {
assert!(!is_public("rustdesk.comhello.com"));
}
#[test]
fn test_is_public_matches_rustdesk_root_domain() {
assert!(is_public("rustdesk.com/"));
assert!(is_public("rustdesk.com:21117"));
assert!(is_public("api.rustdesk.com:21117"));
assert!(!is_public("hello-rustdesk.com"));
assert!(!is_public("api.rustdesk.com.evil.test"));
assert!(!is_public("https://rustdesk.com@evil.test"));
}
#[test]
fn test_should_use_tcp_proxy_for_api_url() {
assert!(should_use_tcp_proxy_for_api_url(

View File

@@ -190,6 +190,9 @@ pub fn core_main() -> Option<Vec<String>> {
crate::platform::elevate_or_run_as_system(click_setup, _is_elevate, _is_run_as_system);
return None;
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
init_plugins(&args);
if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) {
#[cfg(target_os = "macos")]
{
@@ -713,6 +716,14 @@ pub fn core_main() -> Option<Vec<String>> {
// call connection manager to establish connections
// meanwhile, return true to call flutter window to show control panel
crate::ui_interface::start_option_status_sync();
} else if args[0] == "--cm-no-ui" {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
crate::ui_interface::start_option_status_sync();
crate::flutter::connection_manager::start_cm_no_ui();
}
return None;
} else if args[0] == "--whiteboard" {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
@@ -726,6 +737,22 @@ pub fn core_main() -> Option<Vec<String>> {
crate::platform::gtk_sudo::exec();
}
return None;
} else {
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if args[0] == "--plugin-install" {
if args.len() == 2 {
crate::plugin::change_uninstall_plugin(&args[1], false);
} else if args.len() == 3 {
crate::plugin::install_plugin_with_url(&args[1], &args[2]);
}
return None;
} else if args[0] == "--plugin-uninstall" {
if args.len() == 2 {
crate::plugin::change_uninstall_plugin(&args[1], true);
}
return None;
}
}
}
//_async_logger_holder.map(|x| x.flush());
@@ -735,6 +762,23 @@ pub fn core_main() -> Option<Vec<String>> {
return Some(args);
}
#[inline]
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn init_plugins(args: &Vec<String>) {
if args.is_empty() || "--server" == (&args[0] as &str) {
#[cfg(debug_assertions)]
let load_plugins = true;
#[cfg(not(debug_assertions))]
let load_plugins = crate::platform::is_installed();
if load_plugins {
crate::plugin::init();
}
} else if "--service" == (&args[0] as &str) {
hbb_common::allow_err!(crate::plugin::remove_uninstalled());
}
}
fn import_config(path: &str) {
use hbb_common::{config::*, get_exe_time, get_modified_time};
let path2 = path.replace(".toml", "2.toml");

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