Compare commits

...

44 Commits

Author SHA1 Message Date
rustdesk
9343affe0b fix: check the frame QueryInterface result in dxgi capture
Both AcquireNextFrame paths cast the IDXGIResource to ID3D11Texture2D
without looking at the HRESULT. ohgodwhat() then dereferences the null
pointer in GetDesc(), and get_texture() hands a null texture to the vram
encoder. Return the error instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sw75MSAz7PTqrSALdStXe
2026-08-27 15:02:31 +08:00
rustdesk
7c830e76c6 fix: reuse the dxgi staging texture instead of one per frame
ohgodwhat() created a full screen D3D11_USAGE_STAGING texture for every
captured frame and pinned each one with SetEvictionPriority(MAXIMUM).
Because D3D11 resource destruction may be deferred, that per-frame churn
can accumulate a large amount of graphics kernel paged pool on affected
drivers. Keep a single staging texture and rebuild it only when the
desktop image changes shape.

Also check the IDXGISurface QueryInterface result, so a failure can no
longer leave surface null while readable holds a valid texture.

Reported in #15945.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sw75MSAz7PTqrSALdStXe
2026-08-27 15:02:31 +08:00
RustDesk
1fe451c2e8 chore(flutter): bump desktop_multi_window for show recovery (#15959)
Pick up rustdesk-org/rustdesk_desktop_multi_window#37, which re-arms the existing bounded redraw timer whenever a secondary window is shown, including when its first frame was generated while hidden but not presented.

This may perform one delayed child refresh on each show. It intentionally does not add a presentation-complete flag: Flutter reports frame generation rather than successful presentation, so recording success after a synthetic refresh could suppress later self-recovery without a reliable success signal.
2026-08-27 14:42:09 +08:00
fufesou
0b08a83d4b fix(file-transfer): improve large directory loading (#15830)
* fix(file-transfer): improve large directory loading

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file-transfer): avoid failing newer directory reads

Track each remote directory request by its registered completer and only remove
the task when it still matches, preventing stale failures from affecting newer
requests for the same path.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file-transfer): handle slow directory listings safely

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file transfer): correlate directory responses with requests

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file transfer): prevent automatic directory responses from matching requests

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file-transfer): handle large remote directory listings reliably

- build file rows lazily
- register remote reads before sending requests
- handle Home paths, stale responses, errors, and timeouts
- serialize same-path reads with different hidden-file options

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(file transfer): reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: build

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: invalidate pending dir reads on reconnect

Signed-off-by: fufesou <linlong1266@gmail.com>

* test(file-transfer): cover remote directory read lifecycle

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-27 13:01:11 +08:00
rustdesk
e9b81e3475 typo 2026-08-27 11:47:36 +08:00
RustDesk
7220f00410 fix(linux): a Wayland session without XAUTHORITY is not incomplete (#15978)
Fixes #15952.

Hyprland runs Xwayland without exporting `XAUTHORITY`, and
`get_display_xauth_xwayland` only returns once it has both `DISPLAY` and
`XAUTHORITY`. On such a session that condition is never met, so every refresh
runs the retry loop to the end: 10 rounds x 6 process patterns x 4 variables =
240 `get_env` calls, each a `sh -c` pipeline of ~12 processes starting with a
full `ps -u <uid> -f`. That is ~2900 fork/exec per refresh, and the service loop
repeats every 500 ms. The reporter measured a full core on a low-end laptop and
~60% of a core on a 13600KF.

The Wayland side answers for such a session, so accept `DISPLAY` together with
either `XAUTHORITY` or `WAYLAND_DISPLAY` + `DBUS_SESSION_BUS_ADDRESS`. The
portal answers on the first pattern, which ends the walk there, as it already
did on desktops that do export an xauth.

The loop also assigned all four variables unconditionally per pattern, so the
patterns that do not run on a given desktop blanked out what an earlier one had
answered with -- the portal's valid `DISPLAY=:1` included. That is why the
`--server` was then started with no `WAYLAND_DISPLAY` and no
`DBUS_SESSION_BUS_ADDRESS`. Candidates are now taken from one pattern as a whole
and ranked, so a later pattern replaces an earlier answer only by being better,
and a session that can only offer a compositor and a bus still keeps them.

A compositor that starts Xwayland on demand shows the same shape from the other
side: the portal came up before Xwayland did, so its environment carries a valid
`WAYLAND_DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` but no `DISPLAY`, and no pattern
here may ever produce one. That pair alone is a session the child server can be
started against -- it is exactly what `get_display_xauth_wayland` returns on --
so it outranks a bare `DISPLAY` and ends the retrying, while the rest of the
round still looks for something that completes the session.

Not specific to the drm build: the function is not feature-gated, and the commit
the report points at does not touch it.


Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:39:59 +08:00
fufesou
fd471fcf02 fix: show speed in desktop file transfer status (#15980)
* fix: show speed in desktop file transfer status

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: move file transfer speed beside progress bar

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: move file transfer speed into progress bar

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: refine file transfer speed display

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: adapt file transfer progress text colors

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: reduce file transfer speed text weight

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-27 11:08:58 +08:00
Jade
7c6e661fcc fix(linux): Set AppIndicator ID for tray-icon (#15981)
* set static AppIndicator ID in tray-icon init

allows DEs, eg. KDE to 'remember' the user's configuration of tray hidden/unhidden. see: https://github.com/rustdesk/rustdesk/discussions/15208

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>

* Update tray.rs

---------

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-08-27 09:41:24 +08:00
Mariano Abad
3f207e91f6 fix(linux): a session logout should hand the peer to the login screen (#15905)
* fix(linux): a session logout should hand the peer to the login screen

Logging out closes every window in the session, the connection manager's
included, and its close handler kicks every peer with the reason a person
gets when they disconnect one by hand. That reason is the one thing the
client never retries on, so the remote session dies on a frozen frame
instead of reconnecting to the greeter that is already there.

The close carries nothing to tell the two apart: measured on KDE, the CM
receives no signal and logind still reports the session active at that
instant, and the server is killed within a few hundred ms either way, so
neither a state check nor a grace period can decide it. What is
distinguishable is the ACTION: disconnecting a peer is not the same event
as this window going away. So the window-close path now says so, and the
server ends the session without poisoning the retry; the Disconnect
button and the app's own close control keep kicking exactly as before.
Linux only, since that is where a logout closes the window.

Verified on plasma/sddm with a client attached: a logout now reconnects
to the greeter with no dialog, while closing the manager window still
shows Closed manually by the peer.

* fix(linux): close the tunnel too, and keep the web build compiling

Three seams the first pass missed. The web bridge is hand written, not
generated, so the new call needs its stub there or flutter build web
stops compiling - and that job is disabled in CI, so it would have gone
green. try_port_forward_loop is a second consumer of the same channel
and only knew Close, so a forwarded tunnel outlived the window it was
supposed to die with. And the variant had landed inside the DRM section,
whose comment says everything below it is drm-gated.
2026-08-26 18:26:26 +08:00
Kino
cec4085238 Bump aom to v3.14.1 (#15883)
* Bump aom to v3.14.1

* Remove oboe dependency in vcpkg.json
2026-08-25 19:53:56 +08:00
fufesou
0d917c6fa1 fix: remove dup translations (#15967)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-25 18:25:49 +08:00
Rafli Surya Wijaya
893dc27798 docs(readme): fix broken Screenshots section anchor link (#15964) 2026-08-25 11:21:34 +08:00
Abdullah Kaleem
7cc82c1575 Add Urdu language support for UI strings (#15961)
* Add Urdu language support for UI strings till 329 line

Co-authored-by: Copilot <copilot@github.com>

* Add Urdu translations for additional UI strings

* Add Urdu language support in lang.rs

* Fix Urdu translations and remove unused keys in ur.rs

---------

Co-authored-by: Copilot <copilot@github.com>
2026-08-25 09:19:37 +08:00
jhertel
f07b6e2338 Correct Danish spelling, language and translation (#15943)
* Update da.rs

Corrected spelling, language and translation mistakes.

* Update da.rs

Missed one correction.
2026-08-24 17:22:03 +08:00
Robert Markovski
a3bab27a2a fix: Show My Cursor freezes in View Only mode when remote user mo... (#15936) 2026-08-24 17:21:11 +08:00
RustDesk
7423dced37 Update reference from AGENTS.md to @AGENTS.md 2026-08-22 17:50:10 +08:00
fufesou
a7deef02a2 fix(msi): keep only native ProductCode uninstall entry (#15891)
* fix(msi): keep only native ProductCode uninstall entry

Move installer state outside the Uninstall registry path,
clean up legacy duplicate entries, and use the MSI ProductCode
for updates and uninstalling.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): harden update and uninstall handling

- handle legacy EXE updates without an MSI ProductCode
- propagate MsiExec uninstall failures
- validate and XML-quote custom ARP values

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): validate registry state before update and uninstall

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): pass WindowsInstaller state to elevated sequence

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): block unsupported MSI-to-EXE upgrades

- resolve native MSI state and ProductCode safely
- suppress reboot while preserving MSI uninstall results
- publish the resolved ARP install location
- skip invalid unrelated MSI uninstall entries

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): fail uninstall when ProductCode is missing

Prevent known MSI installations from falling back to
EXE cleanup when the ProductCode cannot be resolved.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): do not abort update on ARP version write failure

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-22 17:49:00 +08:00
rustdesk
d5a7f67999 fix appimage pixbuf crash 2026-08-22 12:25:35 +08:00
ben-leone
e266380ee9 fix(appimage): keep the XDG default data dirs on XDG_DATA_DIRS (#15938)
AppRun sets XDG_DATA_DIRS to
"$APPDIR/usr/local/share:$APPDIR/usr/share:$XDG_DATA_DIRS". When the host
leaves XDG_DATA_DIRS unset, the result contains no /usr/share, and setting
the variable at all suppresses the XDG default of /usr/local/share:/usr/share.

gdk-pixbuf 2.43+ (Arch, CachyOS, Gentoo, Fedora, openSUSE) no longer ships PNG,
JPEG or WebP as loader modules; libgdk_pixbuf links libglycin and decodes them
through it, and glycin discovers its loaders in
$XDG_DATA_DIRS/glycin-loaders/<ver>/conf.d/*.conf. With /usr/share missing,
glycin finds none and every PNG decode inside the AppImage fails with
"Unrecognized image file format".

RustDesk sends remote cursors to flutter_custom_cursor as PNG, and that plugin
returns nullptr from a std::string function when the decode fails, so the first
non-default cursor of a session aborts the process:

    GdkPixbuf-CRITICAL **: gdk_pixbuf_copy: assertion 'GDK_IS_PIXBUF (pixbuf)' failed
    terminate called after throwing an instance of 'std::logic_error'
      what():  basic_string::_M_construct null not valid

Debian and Ubuntu compile PNG straight into libgdk_pixbuf and never reach
glycin, which is why this only affects non-Debian hosts.

Append the two XDG defaults so they are present when the host does not provide
them. They go last, so a session that sets XDG_DATA_DIRS properly keeps its own
precedence, and appending is a no-op where those paths are already listed.

Verified on CachyOS (gdk-pixbuf 2.44.7) against a stock 1.4.9 AppImage: with
only this variable changed, a full remote session runs without crashing and
renders remote cursors correctly.

Refs #4565 #5457 #7013 #9164 #10563 #11499 #12257 #14305 #14405 #15625

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 11:21:48 +08:00
Saverio Miroddi
cbf9440281 Prefer active X11 session display (#15933)
* Prefer active X11 session display

* Update linux.rs

* fix(linux): keep the logind display only when it is a local one

`get_display_from_session` returns the value pam_systemd was handed at session
creation, and logind never updates it afterwards. That value is not always a
usable local display: it can be qualified with this host (`myhost:0`), name an
X forwarding endpoint (`localhost:10.0`), or be a bare `:`.

Taking it unconditionally is worse than taking nothing, because a non-empty
`self.display` suppresses every fallback below it, `get_display_by_user` and the
`:0` default alike. The stripping at the end of `get_display_x11` does not save
the last two cases either: it leaves `:` as is and turns `localhost:10.0` into a
local looking `:10.0`, either of which is then exported as DISPLAY and leaves the
session unreachable, where before this PR the host got a working `:0`.

Strip this host so `myhost:0` is still accepted as `:0`, leave `localhost` in
place, and require a display number after the colon. Anything else falls through
to the existing chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

* docs(agents): prefer a little duplication over a restructure

The "Be minimally invasive" rules already ask for purely additive diffs, but not
in the case where the addition would otherwise reshape an existing function so
the two can share code. Repeating a few lines is the better diff there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:28:12 +08:00
rustdesk
6eaac17ac5 typo 2026-08-22 00:02:34 +08:00
fufesou
92eb137178 feat(terminal): use platform-native copy and paste shortcuts (#15931) 2026-08-21 21:20:39 +08:00
palmoni5
61ddade049 fix(windows): restore keyboard focus when the cursor re-enters the remote image (#15880)
* fix(windows): restore keyboard focus when the cursor re-enters the remote image

On Windows the raw key focus node is unfocused on window blur and nothing
requests it back, so returning to an already connected session left the
keyboard dead until the remote image was clicked.

Request focus from enterView(), gated on the window being active, the tab
being selected and no blocking overlay, so a background window cannot grab
system keys. enterOrLeave(true) is still driven by RawKeyFocusScope's
onFocusChange, so it is not called twice.

* fix(windows): refocus on window focus when the cursor already hovers the image

Alt+Tab or a taskbar click returns focus without a PointerEnter, so
enterView() cannot restore the keyboard. Reuse _cursorOverImage, gated
on the selected tab and no blocking overlay.

* refactor(windows): share one focus predicate for every requestFocus path

The relative-mouse-mode restore on window focus could hand remote input
to this page while a blocking dialog was up or the tab was not selected.
2026-08-21 17:07:25 +08:00
fufesou
c78bdefc44 fix: dialog, trackpad speed, buttons (close -> ok, cancel) (#15918)
* fix: dialog, trackpad speed, buttons (close -> ok, cancel)

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(flutter): handle trackpad speed dialog submission

- commit typed values from Enter and OK
- validate input before saving
- prevent duplicate submissions
- surface save failures

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(flutter): sync trackpad speed input and slider

- handle trackpad speed submission from IME actions
- update the slider when a valid speed is typed
- cover Enter, OK, IME, and invalid input behavior

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-21 14:15:35 +08:00
rustdesk
c45f7d2dd2 refactor is_public 2026-08-21 01:25:51 +08:00
RustDesk
798b73beb1 Update common.rs (#15924) 2026-08-21 00:52:31 +08:00
rustdesk
f1a06f6765 review rules 2026-08-20 19:22:36 +08:00
fufesou
0a4b431ea2 fix: correct terminal mouse selection and scroll coordinates (#15915)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-20 13:10:45 +08:00
21pages
5679670506 fix(flutter): make Adjust Window reliable across desktop platforms (#15853)
* fix(flutter): make Adjust Window reliable across desktop platforms

  - Fix incorrect sizing on scaled displays by calculating the target from the
    rendered canvas scale and platform-specific window coordinate units.
  - Fix adjustments using the wrong monitor by querying the current remote
    window's screen, with the main window as fallback.
  - Fix stale geometry after fullscreen or maximized transitions by refreshing
    metrics before calculating and applying the target frame.
  - Fix fullscreen availability checks on Windows and macOS by predicting the
    restored window borders and caching each macOS window's pre-fullscreen work area.
  - Fix incorrect Linux work areas by handling GNOME Wayland fractional scaling
    and caching compositor/X11 work-area measurements when visibleFrame is wrong.
  - Prevent unsafe adjustments by rejecting invalid, oversized, or implausibly
    small target frames.
  - Avoid failures during window teardown by skipping adjustment when the view,
    screen, or native window frame is unavailable.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden Adjust Window handling

  - Use the dynamic Linux resize edge when predicting restored window bounds.
  - Treat GNOME fractional-scaling lookup failures as unknown without repeating
    the lookup for the remote window.
  - Stop adjustment safely when native window calls fail during window teardown.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): correct Linux monitor selection

Update window_size to use monitor height for vertical bounds, preventing incorrect screen selection with vertically stacked displays.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* docs(flutter): simplify Linux screen handling comments

Keep the source rationale concise and move platform measurements and investigation details out of the implementation.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): align Adjust Window resize padding

Use the shared drag-to-resize padding for Linux restored-window predictions so menu validation matches the applied frame dimensions.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove Adjust Window screen fallback

Return null when the current window screen is unavailable instead of using the main window's scale factor and work area.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): query Mutter monitor layout mode

  Use DisplayConfig.GetCurrentState instead of inferring scaling from
  experimental features, and handle Ubuntu's UI-scaled logical mode.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): use native maximized state for Wayland cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): allow Adjust Window to fill work area

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): avoid racing screen info updates

Signed-off-by: 21pages <sunboeasy@gmail.com>

* refactor(flutter): remove dead Adjust Window web plumbing

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): tolerate near-unity Wayland scale factors

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden window screen detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): drop deprecated GNOME session detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove GNOME monitor layout mode flutter cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-08-19 15:08:58 +08:00
Mariano Abad
630b531108 fix(flutter): initialize the cursor hotspot y from its own origin (#15898)
The CursorData constructor copies hotxOrigin into hoty. Latent today:
both consumers call updateGetKey() before reading, and _checkUpdateScale
recomputes hoty from hotyOrigin - but any future read before that call
inherits the x value silently.
2026-08-19 12:49:28 +08:00
RustDesk
1984678785 Hide printer tab when settings disabled (#15901)
* fix: hide the printer settings tab when settings are disabled

The Security and Network tabs already honour `disable-settings`, but the
Printer tab was gated only on `hide-remote-printer-settings`, so custom
clients built with settings disabled still exposed it.

https://github.com/rustdesk/rustdesk-server-pro/issues/1001

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: add hide-general-settings builtin option

Hides the General tab of the settings page. Unlike the other
hide-*-settings options this one is still useful when settings are
disabled, since `disable-settings` does not cover the General tab.

https://github.com/rustdesk/rustdesk-server-pro/issues/1001

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:48:30 +08:00
fufesou
b0008edcb5 refact: remove linux headless (#15866)
* refact: remove linux headless

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(linux): probe DRM availability asynchronously on login

Signed-off-by: fufesou <linlong1266@gmail.com>

* revert changes in drm_capturer.rs

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update submodule hbb_common

Signed-off-by: fufesou <linlong1266@gmail.com>

* docs(linux): clarify DRM availability comments

Remove stale headless and unauthenticated-request
wording, and document the Available-only login-screen gate.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(linux): remove unreachable session cleanup branch

Remove the obsolete empty-session path and
clarify the intended use of cached DRM availability.

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-18 15:02:50 +08:00
rustdesk
0c00d576dd improve comment rules 2026-08-18 12:31:59 +08:00
RustDesk
9b1b810d3a Delete .github/dependabot.yml (#15888) 2026-08-18 10:40:04 +08:00
RustDesk
6a27910f34 fix(wayland): back off the polling display lookups after a failure (drm) (#15865)
* fix(wayland): back off the polling display lookups after a failure (drm)

In drm builds an enumeration that fails with no endpoint named in the
environment falls back to the socket probe, which forks a child bounded by
seconds, and the display service asks again every 300 ms -- at a greeter
with no reachable compositor that is a probe child per turn, forever. Such
a failure now stamps a shared 5 s backoff, and only the polling callers
honor it: the 300 ms displays-changed check skips its turn and the 1.5 s
live layout poll returns no answer for that turn.

Only the failure that would fork stamps. A session server is spawned with
WAYLAND_DISPLAY set, so its failed connect bails in-process before any
fork; stamping there would buy nothing and cost recovery latency, so live
sessions keep master's behavior exactly. The stamp also survives
clear_wayland_displays_cache: it describes the seat, not the cache, and
the ~1/s capturer rebuild loop clears on every teardown -- dropping the
stamp with the cache would let that loop defeat the backoff and would
turn every post-hotplug failure into a "first" one forever.

The displays-changed check weighs the backoff against what is already
published. With nothing synced yet it always populates -- an unaugmented
DRM list beats the empty broadcast the send path would otherwise emit.
With a synced layout, a suppressed turn keeps it, and a fresh first
failure keeps it too; only a failure that persists across a backoff
replaces it with the DRM stack, so a hotplug at a failing seat converges
within one backoff while a transient failure never tears down a good
layout.

One-shot callers -- session init, pipewire stream setup, capturer info --
keep probing fresh through get_displays, whose failure semantics are
unchanged: replaying a transient failure there would latch an empty answer
into session-long state. Non-drm builds compile none of this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(wayland): log DRM lookup failure once

* fix(wayland): reset lookup warning after recovery

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 09:57:06 +08:00
rustdesk
14a4b197ad translations 2026-08-17 17:22:57 +08:00
Krik JIN
8ffe3117a5 feat(flutter): add mobile canvas lock (#15877)
* feat: add mobile canvas lock

* Update flutter/lib/models/model.dart

Remove redundant canvas-lock comment

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove redundant logic

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Krik Jin <isjinhk@outlook.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-17 17:04:58 +08:00
RustDesk
5a78be03e3 feat(rdp): title the mstsc window after the peer instead of "localhost" (#15781)
* feat(rdp): title the mstsc window after the peer instead of "localhost"

The RDP tunnel launched `mstsc /v:localhost:<port>`, so with several
sessions open every window is titled "localhost" and servers cannot be
told apart.

mstsc titles the session window after the launched .rdp file's base
name, so write a temp .rdp file (containing only the tunnel address)
named after the peer alias, cached hostname, or id, and launch that
instead. Falls back to the old /v: form when no usable name remains
after filename sanitization or the file cannot be written. Credential
handling is unchanged: cmdkey targets "localhost", which is still the
host mstsc resolves credentials against.

Fixes rustdesk/rustdesk#15775 (discussion)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(rdp): set mstsc title without temporary files

  Keep launching mstsc with /v so Default.rdp settings are preserved
  and unsigned RDP file warnings and policy restrictions are avoided.

  Track the launched mstsc process and reapply the peer name when the
  window title is reset during connection or reconnection.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* docs(rdp): clarify mstsc title limitation

Signed-off-by: 21pages <sunboeasy@gmail.com>

* feat(rdp): show peer identity with hostname in mstsc title

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-08-17 13:30:18 +08:00
fufesou
edd0e5fbd4 fix(CI): rust 1.75, linux sciter (#15874)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-17 09:30:19 +08:00
yzxcj797
3871c47855 docs: remove stale flutter/web/js entry and fix dead localized build links (#15869) 2026-08-17 09:18:45 +08:00
yzxcj797
8d52d48b24 docs: fix dead code of conduct links in ID/IT contributing guides (#15868) 2026-08-17 09:18:21 +08:00
RustDesk
7aa98d43cf Refact/plugin removal leftovers (#15864)
* fix(flutter): dispose the settings PageController and order dispose() correctly

`dispose()` began with `super.dispose()`, so the mixin chain marked the State
defunct before the WidgetsBindingObserver registration and the periodic timer
were released. The `PageController` was never disposed at all: `Get.delete`
only runs `onDelete()` for a `GetLifeCycleBase`, and a plain `ChangeNotifier`
is not one, so every open/close of the Settings tab leaked one controller with
its listener still attached.

Also guard `switch2page` on the `Rx<SettingsTabKey>` registration it actually
reads rather than only the `PageController` — now that both are really
deleted, a partial teardown would throw into the catch and silently open the
wrong tab — and re-check `mounted` after the await in the `_videoConnTimer`
tick, which `Timer::cancel` cannot stop once the body has started.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refact: finish the plugin-framework removal sweep

#15854 removed the feature but stopped short of its leftovers:

- `Uninstall`, `Enable`, `Disable`, `Options` and `Please install plugins`
  were consumed only by the deleted `flutter/lib/plugin/**`; drop them from
  template.rs and the 50 locale files (250 dead entries). `Update` and
  `Install` stay, still used by desktop_home_page.dart.
- The server no longer sends `PrvOnFailedPlugin`, and the client no longer
  offers to install plugins when privacy mode fails to turn on.
- Drop the MSI `F_Client_Plugins` / `F_Server_Plugins` localization strings;
  no `.wxs` references them.
- `_DisplayMenu`'s constructor became a pure pass-through once `pluginItem`
  was removed, and the cfg inside `handle_input` repeats the one on the
  function itself.
- Normalize `src/lang/sl.rs` to 0644, the only executable file under src/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): handle legacy privacy mode plugin failures

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-14 18:52:03 +08:00
fufesou
d1da05c4db refact: remove feature plugin-framework (#15854)
* refact: remove feature plugin-framework

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: remove unused translations

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: delete settings tab observable with correct type

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-14 14:31:13 +08:00
Mariano Abad
d829d1410a fix(linux): serve the Wayland login screen the DRM backend was built for (#15792)
* fix(linux): serve the Wayland login screen the DRM backend was built for

The login screen support in #15420 never worked on a real greeter. fufesou found
it: the session is refused, and with the refusal commented out the client gets a
failed connection instead of a screen.

One premise under all of it. `get_values_of_seat0` is
`_get_values_of_seat0(.., ignore_gdm_wayland = true)`, so a gdm/sddm Wayland
session is skipped by construction and `get_display_server` falls back to x11.
That was correct while the portal was the only backend, since the portal cannot
serve a greeter at all. The DRM path never talks to the compositor, which is
precisely why it can serve one, so the premise stops holding there and every
x11-vs-Wayland decision in the tree answers x11 at a login screen.

The central change is the memoised `IS_X11`: when it reads x11 and seat0 is a
Wayland greeter, answer Wayland. That covers fifteen routing sites at once, and
it is under `cfg(feature = "drm")`, so a build without the backend keeps the
current answer exactly. `is_x11_for_drm` is the unmemoised form for the two
retry loops that must keep asking while a boot is still naming the session, and
the memoised accessor is scoped to per-frame callers in the per-session
`--server`, which the service only spawns once it has identified the session.

Input was the last layer and lived outside all of that. `Enigo` decides
x11-vs-Wayland once in `Default::default()`, from the same seat0 lookup, and on
"x11" routes every key and mouse event to xdo; with no X server that context is
null and libxdo drops them without an error. So the uinput devices were created,
the compositor opened them, and nothing was ever written to them. `set_is_x11`
is now called where the custom devices are installed, which is only reached once
`!is_x11()` is already established. The unit test pins both directions, since a
one-directional test passes against the bug.

With no compositor reachable, the uinput desktop rect comes from the DRM display
list instead: those are the same displays being captured, so the coordinate space
matches by construction. Telling the truth about a greeter also makes four
compositor-probing paths reachable where the probe cannot answer; all four
already treat an empty output list as "nothing to do", so they skip it and 11818
"Could not find wayland compositor" warnings in one session became 1.

Tested on an sddm Plasma Wayland greeter, MacBook T2, 2880x1800: the greeter
renders, typing from the client enters characters in the password field, a click
at an absolute coordinate opens the greeter session combo, the service pre-warm
primes in 994 us instead of timing out, and the privileged service maps no EGL
during a live capture. Not proven on gdm under Wayland.

Known limitations: non-ASCII characters cannot be typed at a greeter, because
that path goes through the clipboard and the clipboard here is X11 only; and at
a multi-monitor greeter the pointer reaches the first display only, since every
DRM output reports origin (0,0) on Wayland and there is no arrangement to derive
without the compositor.

* fix(linux): a Wayland greeter the DRM backend can serve is not headless

fufesou reported the login screen still failing on Ubuntu 24.04 with gdm3, with
the client asking for OS credentials to start an X session instead of showing the
greeter. Reproduced on a real gdm greeter here.

Same premise as the rest of the branch, one more consumer. `DesktopManager::new`
reads seat0 through `get_values_of_seat0`, which skips a gdm/sddm Wayland session
by construction, so at a greeter it finds no session at all and
`get_supported_display_seat0_username` returns None from its empty-username arm.
That makes `is_headless()` true, so the service advertises headless and
`try_start_desktop` answers `LOGIN_MSG_DESKTOP_SESSION_NOT_READY`. The corrected
`IS_X11` does not reach this one: it asks who owns seat0, not which display
server is running.

So ask again, with the greeter visible, when the DRM backend can capture and
inject into it. At query time rather than in `new()`, because the DRM probe has
not necessarily settled when the desktop manager is constructed, and the answer
would latch for the process lifetime. In a normal session the latched username is
a real user and the extra read is skipped.

* chore: drop the hbb_common bump, this branch does not need it

The bump carried rustdesk/hbb_common#580, the compositor-socket fallback. Nothing
here depends on it: the greeter paths in this branch are the ones that run when
compositor data is unavailable, which is what the commit before this one states as
a known limitation. Keeping the bump would only block the greeter fix behind a
review of a separate change, and would import that change's blocking review items
into this path.

* fix(linux): let the uinput uid gate see the greeter that owns seat0

Input at a real greeter was rejected by our own authorization. Measured on Ubuntu
24.04 with gdm3: the root service logs

  Rejected unauthorized connection on uinput ipc channel:
  postfix=_uinput_control, peer_uid=Some(120), active_uid=None

and the greeter's `--server` gets ECONNRESET out of `setup_uinput`, so no uinput
device is ever created and neither keyboard nor mouse reaches the greeter.

uid 120 is gdm, the owner of the only active seat0 session. `active_uid` is None
because the uinput authorizer deliberately bypasses the service-loop cache and
takes a fresh seat0 lookup, and the fresh read hides a Wayland greeter by
construction. The cache-based gates do not have the problem: `Desktop::refresh`
fills it through the greeter-visible read, which is also why capture and config
sync work at a greeter while input does not.

So make the fresh read agree with the cache. It keeps the property the uinput gate
wants, a lookup that cannot be stale, and it still compares the peer against the
uid of the session that owns seat0 -- which at a greeter is the greeter.

* fix: settle the DRM probe before routing login to X11, and read seat0 fresh

Two findings from the #15792 review, both verified against the code:

- drm_login_screen_seat0_username asked the cached probe, so a client
  arriving before warm_availability publishes its verdict read "no DRM"
  and, with allow-linux-headless=Y, try_start_x_session could start Xorg
  over a live Wayland greeter. Ask the probing form instead, and only
  after the cheap seat0 read says a Wayland greeter is actually there: a
  bounded definitive verdict is affordable on a login-time path.

- get_supported_display_seat0_username trusted the seat0 values cached in
  DesktopManager::new(), which go stale across a logout or a fast user
  switch: a stale non-greeter name skipped the greeter probe and was
  returned as the supported display owner. Read seat0 fresh on every
  query; every call site is connection-time, so the extra loginctl read
  is cheap.

Regression-tested on a real sddm Wayland greeter: capture streams the
greeter, the RustDesk password dialog is the only prompt, and five typed
characters appeared in the greeter password field over uinput with zero
"Rejected unauthorized connection" lines in the service log.

* fix: ask the greeter compositor for the multi-monitor layout

The display arrangement and the pointer mapping were wrong at a
multi-monitor login screen, and the mechanism is measured on a two-head
virtio VM: DRM has no origins, so every display was advertised at (0,0)
(a stacked arrangement on the client), and the uinput range was taken
from the union of the DRM modes while the compositor had arranged the
outputs side by side.

Both came from the same premise, written before the hbb_common socket
fallback existed: "a login screen has no compositor to ask".
wayland_outputs_askable() skipped the wl_output augmentation at any
greeter, and update_uinput_resolution took the DRM union directly. The
premise is false now: a greeter runs a compositor, and the socket
fallback reaches it with no environment variables, measured answering
two outputs at the VM greeter while the old gate was still routing
around it.

Drop the gate and take the compositor-first path everywhere. Where the
fallback cannot answer, the output list comes back empty and both call
sites degrade to exactly the old behavior, so a build against an older
hbb_common is unchanged.

* fix: augment a single display too, and probe the desktop rect off the executor

Two follow-ups from the automated re-review of cd80c3dee, both verified:

- augment_with_wayland_geometry skipped the compositor below two DRM
  displays, but on a multi-GPU host the one connector this service can
  open may sit at a non-zero origin of the compositor layout, and DRM
  alone reports (0,0).

- the desktop rect for uinput can now block for the socket probe
  deadline, and update_uinput_resolution runs on current-thread
  runtimes; move the query into spawn_blocking.

The third re-review finding, the warm-up allegedly skipping Wayland
greeters, is refuted: warm_availability probes while is_x11_for_drm()
is false, which includes a Wayland greeter, and the greeter log of the
VM run behind cd80c3dee shows the warm succeeding there.

* fix: baseline the layout from the blocking task, and augment a lone output's origin

The layout snapshot after the rect lookup still ran on the executor: a
failed compositor lookup is not cached, so the snapshot synchronously
repeated the whole socket probe there. The baseline is now computed
inside the same blocking task, from the snapshot the successful lookup
just cached, or omitted when only the raw DRM union was available,
which keeps the #15601 remap inactive exactly where origins are
unknown.

A single compositor output now hands its origin to a single connector:
the lone output can sit at a non-zero origin the DRM side cannot see.
Scale stays 1 on purpose, matching how a single display is advertised
at physical size, and more connectors than the one output stays
unaugmented, since the layout-order fallback would plant that origin on
a guess.

Also refresh the get_primary_index doc that still said augmentation
declines below two connectors.

* fix: read the DRM probe as a tri-state, and keep pre-auth seat0 checks cache-only

is_available() answered false both for a definitive no-DRM verdict and
for a probe that had simply not settled (another probe in flight, or a
failure still below the disable threshold), and the login-screen
decision turned that transient false into no-greeter: try_start_x_session
could put Xorg over a live greeter in exactly the window the probe
needed. The machinery now answers Available/Unavailable/Unsettled, and
only a definitive Unavailable routes the seat toward X11.

Connection setup also ran the whole lookup pre-auth: constructing
LinuxHeadlessHandle called is_headless() before authentication, holding
DESKTOP_MANAGER while loginctl ran and, at a greeter, while the DRM
probe waited out its handshake. An unauthenticated peer could occupy a
worker for seconds and serialize every other connection on the mutex.
is_headless() now answers from a snapshot refreshed off-thread, and the
fresh lookup became a free function called with the manager lock
released everywhere; the enforcing decisions, get_username and
try_start_x_session, still read seat0 fresh.

Also drops seat0_display_server, dead since the fresh-read change.

* fix: respect RUSTDESK_FORCED_DISPLAY_SERVER over the greeter correction

The greeter correction rewired IS_X11 and is_x11_for_drm() to Wayland
whenever seat0 looks like a Wayland greeter, including when the operator
explicitly forced the display server: get_display_server() kept honoring
the override while the DRM routing gates contradicted it, leaving
capture and input routing internally inconsistent. The correction now
only adjusts the auto-detected answer.

* fix: honest pre-auth snapshot, sticky negative verdict, and a complete forced-x11 gate

Four defects found by an adversarial review of the two previous
commits, all in their new lines:

- The empty-snapshot fallback derived headless from the manager's
  boot-time seat0 read, which is blank at a Wayland greeter (the
  loginctl wrapper skips greeter sessions), so the first connection of
  every server process at a greeter answered headless=true, the
  opposite of the comment on it. No snapshot now answers NOT headless,
  the snapshot is seeded at start_xdesktop, and the boot-time cache is
  gone entirely (it had no reader left).

- wait_desktop_cm_ready gated on a bool stored at construction, which
  can lag one seat0 transition behind and skipped the CM-ready wait
  right after a logout. It re-reads the snapshot at call time.

- A settled Unavailable was erased at NEGATIVE_TTL expiry (state to
  Unknown, failure counter to zero), so a permanently helper-less box
  reopened the Unsettled window every 30 seconds and the login decision
  kept adopting a greeter nothing can serve. The verdict now stays
  Unavailable while an off-thread re-probe re-verifies it: a failed or
  empty re-probe restamps the no, and only a non-empty list flips it.

- The forced-x11 gate only covered IS_X11 and is_x11_for_drm, while
  the seat0 adoption path still probed DRM and admitted greeter
  sessions whose capture and input then routed to X11. Greeter
  adoption now yields to an operator-forced X11, degrading to upstream
  behavior: the connection is refused at the login screen.

* fix: keep the login request path off the probe entirely

try_start_desktop runs while handling a LoginRequest, before password
validation, and at a Wayland greeter its seat0 lookup reached the
probing availability form: an unauthenticated peer could park a worker
for the probe deadline. The greeter adoption now reads a cached
tri-state that never blocks; when the state is Unknown it kicks the
probe off-thread and answers Unsettled, which the login decision treats
as a possibly servable greeter until it settles. Settling lives in the
startup warm-up, that kick, and the TTL re-verifiers; the blocking form
stays for the capture-side callers, where waiting is acceptable.

* fix: run the pre-auth desktop start off the executor, guard the refresh flag, trim comments

From fufesou's #15792 re-review (no blocking issues) plus a bot pass:

- try_start_desktop now runs on spawn_blocking. It executes loginctl,
  and PAM when a session must start, while handling a LoginRequest
  before password validation, so a slow logind must not tie up an async
  request worker; the blocking pool absorbs it.

- kick_seat0_refresh releases SEAT0_REFRESH_IN_FLIGHT through an RAII
  guard, so a panic in the refresh thread cannot freeze is_headless on a
  stale snapshot for the process lifetime.

- drm_can_serve_login_screen stays Available-only, and the reason is now
  in the code: it is deliberately not symmetric with the seat0 adoption
  gate. Adoption yields Xorg only on a definitive Unavailable; admission
  accepts only on a definitive Available; both wait through an unsettled
  probe. Admitting there would black-screen a client on a helper-less
  box, so a review suggestion to make them agree is declined.

- Trimmed two over-long comments to the repo's three-line rule.

* fix(linux): harden DRM login-screen startup

Keep unauthenticated headless checks cache-only, bound OS-session startup to one blocking task, and surface JoinError failures.

Wire the isolated Wayland probe consumer and update hbb_common plus libdrmtap 0.5.4.

* fix(linux): headless refresh state

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(linux): keep headless startup state consistent

- gate concurrent desktop startup attempts
- route CM IPC after refreshing desktop state
- avoid blocking seat0 queries in the CM retry loop
- preserve newer seat0 snapshots during overlapping refreshes
- derive DRM geometry and primary display from one Wayland snapshot

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: rustdesk <info@rustdesk.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-13 20:22:41 +08:00
196 changed files with 4408 additions and 8914 deletions

View File

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

View File

@@ -5,7 +5,7 @@ env:
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
on:
workflow_dispatch:
@@ -124,7 +124,6 @@ jobs:
gcc \
git \
g++ \
libpam0g-dev \
libasound2-dev \
libunwind-dev \
libgstreamer1.0-dev \

View File

@@ -36,13 +36,13 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2025.08.27
# vcpkg version: 2026.07.29
# If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
# Or we may face build issue like
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.9"
NDK_VERSION: "r28c"
@@ -1009,7 +1009,6 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1283,7 +1282,6 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1574,7 +1572,6 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -1896,7 +1893,6 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -2126,7 +2122,6 @@ jobs:
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
liblzma-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
@@ -2203,7 +2198,7 @@ jobs:
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
registry = 'sparse+https://index.crates.io/'
""" > ~/.cargo/config
cat ~/.cargo/config
# install dependencies from vcpkg

View File

@@ -16,7 +16,7 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
VERSION: "1.4.9"
NDK_VERSION: "r26d"
#signing keys env variable checks
@@ -271,7 +271,6 @@ jobs:
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \

View File

@@ -63,17 +63,24 @@
### Comments
* 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.
* 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.
### 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,27 +986,6 @@ 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"
@@ -1477,8 +1456,8 @@ dependencies = [
"compression-core",
"flate2",
"memchr",
"zstd 0.13.1",
"zstd-safe 7.1.0",
"zstd",
"zstd-safe",
]
[[package]]
@@ -1549,12 +1528,6 @@ 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"
@@ -3818,14 +3791,14 @@ dependencies = [
"toml 0.7.8",
"tungstenite",
"url",
"users 0.11.0",
"users",
"uuid",
"webpki-roots 1.0.9",
"webrtc",
"whoami",
"winapi 0.3.9",
"x11 2.21.0",
"zstd 0.13.1",
"zstd",
]
[[package]]
@@ -5959,37 +5932,6 @@ 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"
@@ -6057,35 +5999,12 @@ 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"
@@ -7316,7 +7235,6 @@ dependencies = [
"once_cell",
"openssl",
"os-version",
"pam",
"parity-tokio-ipc",
"percent-encoding",
"piet",
@@ -7365,7 +7283,6 @@ dependencies = [
"wol-rs",
"x11-clipboard 0.8.1",
"x11rb 0.12.0",
"zip",
]
[[package]]
@@ -8907,7 +8824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
dependencies = [
"base32",
"constant_time_eq 0.2.6",
"constant_time_eq",
"hmac",
"rand 0.8.5",
"sha1",
@@ -9352,16 +9269,6 @@ 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"
@@ -11157,52 +11064,13 @@ 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 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",
"zstd-safe",
]
[[package]]

View File

@@ -37,7 +37,6 @@ 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",
@@ -81,7 +80,6 @@ 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"
@@ -191,7 +189,6 @@ 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}
@@ -212,7 +209,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", "examples/custom_plugin"]
exclude = ["vdi/host"]
# 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,7 +19,6 @@ RUN apt update -y && \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libasound2-dev \
libpam0g-dev \
libpulse-dev \
make \
wget \

View File

@@ -3,7 +3,7 @@
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
<a href="#screenshots">Screenshots</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
</p>
@@ -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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -168,7 +168,6 @@ 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,7 +58,6 @@ AppDir:
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
@@ -77,6 +76,13 @@ 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,7 +61,6 @@ AppDir:
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
@@ -80,6 +79,13 @@ 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, libpam0g, 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, 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.2.
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.4.
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
LIBDRMTAP_SHA_PINNED = '653de8c774bc245eaf960611ca7a136f7a544d21'
LIBDRMTAP_SHA_PINNED = '5da68a3a368db569716d0d0f11cefacbb11b2290'
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,8 +704,6 @@ 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/')
@@ -724,12 +722,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/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
@@ -1132,13 +1124,7 @@ 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

@@ -72,7 +72,6 @@ fn install_android_deps() {
path.join("lib").to_str().unwrap()
);
println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe");
println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES");
}

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-ID.md>
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.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-IT.md
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Comunicazioni

View File

@@ -160,7 +160,6 @@ 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,7 +144,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -168,7 +168,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -163,7 +163,6 @@ 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,7 +146,6 @@ 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,7 +158,6 @@ 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/hu/dev/build/)
## [Építés](https://rustdesk.com/docs/en/dev/build/)
## Hogyan építs Linuxon
@@ -150,7 +150,6 @@ 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,7 +162,6 @@ 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,7 +166,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -168,7 +168,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -163,7 +163,6 @@ 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,7 +155,6 @@ 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 libpam0g-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
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -166,7 +166,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -168,7 +168,6 @@ 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/ru/dev/build/)
## [Сборка](https://rustdesk.com/docs/en/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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -170,7 +170,6 @@ 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
## Скриншоты
@@ -180,4 +179,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,7 +166,6 @@ 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 libpam0g-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
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
```
### Arch (Manjaro)
@@ -160,7 +160,6 @@ 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,7 +148,6 @@ 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,7 +220,6 @@ 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,18 +21,6 @@
}
]
},
{
"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",
@@ -63,4 +51,4 @@
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Flatpak"
]
}
}

View File

@@ -84,8 +84,6 @@ 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,26 +936,19 @@ void enterPasswordDialog(
);
}
void enterUserLoginDialog(
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
void enterUserLoginDialog(SessionID sessionId,
OverlayDialogManager dialogManager, String osAccountDescTip) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
void enterUserLoginAndPasswordDialog(
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
void enterUserLoginAndPasswordDialog(SessionID sessionId,
OverlayDialogManager dialogManager, String osAccountDescTip) async {
await _connectDialog(
sessionId,
dialogManager,
@@ -963,7 +956,6 @@ void enterUserLoginAndPasswordDialog(
osPasswordController: TextEditingController(),
passwordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
@@ -974,7 +966,6 @@ _connectDialog(
TextEditingController? osPasswordController,
TextEditingController? passwordController,
String? osAccountDescTip,
bool canRememberAccount = true,
}) async {
final errUsername = ''.obs;
var rememberPassword = false;
@@ -982,11 +973,6 @@ _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) {
@@ -1014,12 +1000,6 @@ _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,
@@ -1096,16 +1076,6 @@ _connectDialog(
controller: osPasswordController,
autoFocus: false,
),
if (canRememberAccount)
rememberWidget(
translate('remember_account_tip'),
rememberAccount,
(v) {
if (v != null) {
setState(() => rememberAccount = v);
}
},
),
],
);
}
@@ -1542,91 +1512,6 @@ 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,
@@ -2014,26 +1899,110 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}
trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
int initSpeed = ffi.inputModel.trackpadSpeed;
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;
final curSpeed = SimpleWrapper(initSpeed);
final btnClose = dialogButton('Close', onPressed: () async {
if (curSpeed.value <= kMaxTrackpadSpeed &&
curSpeed.value >= kMinTrackpadSpeed &&
curSpeed.value != initSpeed) {
await bind.sessionSetTrackpadSpeed(
sessionId: sessionId, value: curSpeed.value);
await ffi.inputModel.updateTrackpadSpeed();
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);
}
}
ffi.dialogManager.dismissAll();
});
msgBoxCommon(
ffi.dialogManager,
'Trackpad speed',
TrackpadSpeedWidget(
value: curSpeed,
return CustomAlertDialog(
title: Text(
translate('Trackpad speed'),
style: TextStyle(fontSize: 21),
),
[btnClose]);
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,
);
});
}
void deleteConfirmDialog(Function onSubmit, String title) async {

View File

@@ -115,6 +115,7 @@ 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) {
@@ -471,6 +472,8 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (canvasLocked) return;
if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale;

View File

@@ -253,8 +253,18 @@ 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});
TrackpadSpeedWidget({
Key? key,
required this.value,
this.onDebouncer,
this.onTextChanged,
this.onTextSubmitted,
});
@override
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
@@ -276,6 +286,34 @@ 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
@@ -315,12 +353,8 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
controller: _controller,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
onSubmitted: (text) {
int? v = int.tryParse(text);
if (v != null) {
updateValue(v);
}
},
onChanged: updateTextValue,
onSubmitted: submitTextValue,
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)),
);
}
// osAccount / osPassword
// osPassword
if (isDefaultConn && perms['keyboard'] != false) {
v.add(
TTextMenu(
child: Row(children: [
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
Text(translate('OS Password')),
]),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
@@ -363,18 +363,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
if (pi.isHeadless) {
showSetOSAccount(sessionId, ffi.dialogManager);
} else {
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
}
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
},
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
),
),
onPressed: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordAction(sessionId, ffi.dialogManager),
onPressed: () => handleOsPasswordAction(sessionId, ffi.dialogManager),
),
);
}

View File

@@ -18,7 +18,6 @@ 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 =
@@ -55,7 +54,6 @@ 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";
@@ -164,7 +162,6 @@ 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";
@@ -193,6 +190,7 @@ 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 =
@@ -325,10 +323,11 @@ 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(5.0)
: EdgeInsets.all(kDragToResizeAreaPaddingSize)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0;

View File

@@ -16,7 +16,6 @@ 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';
@@ -111,7 +110,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
},
),
buildPluginEntry(),
];
if (isIncomingOnly) {
children.addAll([
@@ -782,13 +780,6 @@ 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());
@@ -890,21 +881,6 @@ 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,8 +17,6 @@ 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';
@@ -55,7 +53,6 @@ enum SettingsTabKey {
safety,
network,
display,
plugin,
account,
printer,
about,
@@ -64,7 +61,8 @@ enum SettingsTabKey {
class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (bind.mainGetBuildinOption(key: kOptionHideGeneralSetting) != 'Y')
SettingsTabKey.general,
if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
@@ -74,10 +72,9 @@ 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,
@@ -95,7 +92,8 @@ class DesktopSettingPage extends StatefulWidget {
if (index == -1) {
return;
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag) &&
Get.isRegistered<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller =
Get.find<PageController>(tag: _kSettingPageControllerTag);
@@ -163,17 +161,23 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
if (!mounted) {
return;
}
_canBeBlocked.value = await canBeBlocked();
final blocked = await canBeBlocked();
if (!mounted) {
return;
}
_canBeBlocked.value = blocked;
});
}
@override
void dispose() {
super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
WidgetsBinding.instance.removeObserver(this);
_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();
}
List<_TabInfo> _settingTabs() {
@@ -196,10 +200,6 @@ 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,9 +233,6 @@ 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;
@@ -591,10 +588,6 @@ 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,
@@ -2255,51 +2248,6 @@ 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

@@ -278,7 +278,39 @@ class _FileManagerPageState extends State<FileManagerPage>
item.state != JobState.inProgress,
child: LinearPercentIndicator(
animateFromLastPercent: true,
center: Text(item.percentText),
center: SizedBox.expand(
child: ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) =>
LinearGradient(
colors: [
Colors.white,
Colors.transparent,
],
stops: [item.percent, item.percent],
).createShader(bounds),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text.rich(
TextSpan(
text: item.percentText,
children: [
if (item.recvJobRes)
TextSpan(
text:
' ${readableFileSize(item.speed)}/s',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w300,
color: MyTheme.darkGray,
),
),
],
),
),
),
),
),
barRadius: Radius.circular(15),
percent: item.percent,
progressColor: MyTheme.accent,
@@ -1094,6 +1126,7 @@ class _FileManagerViewState extends State<FileManagerView> {
return element.name.contains(_searchText.value);
}).toList(growable: false)
: entries;
// Keep rows lazy so large directories only build visible list items.
final rows = filteredEntries.map((entry) {
final sizeStr =
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
@@ -1276,7 +1309,7 @@ class _FileManagerViewState extends State<FileManagerView> {
],
))),
);
}).toList(growable: false);
});
return Column(
children: [
@@ -1292,7 +1325,7 @@ class _FileManagerViewState extends State<FileManagerView> {
controller: scrollController,
itemExtent: kDesktopFileTransferRowHeight,
itemBuilder: (context, index) {
return rows[index];
return rows.elementAt(index);
},
itemCount: rows.length,
),

View File

@@ -182,7 +182,6 @@ 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((_) {
@@ -274,6 +273,11 @@ 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;
}
@@ -514,6 +518,15 @@ 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) {
@@ -524,7 +537,7 @@ class _RemotePageState extends State<RemotePage>
_cursorOverImage.value = true;
_macOSLocalFocusLost = false;
}
} else {
} else if (!isWindows || _windowsCanFocusRemoteInput) {
_rawKeyFocusNode.requestFocus();
}
_ffi.inputModel.onWindowFocus();
@@ -836,7 +849,16 @@ class _RemotePageState extends State<RemotePage>
_macOSLocalFocusLost = false;
stateGlobal.getInputSource(force: true);
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
} else if (!isWindows) {
} 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 {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}

View File

@@ -22,6 +22,14 @@ import '../../models/file_model.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
/// Set only by this window's own close control, and only once the user has confirmed. Any other
/// way the window can go - a session logout closing every window, the window manager, a native
/// title-bar button this app does not draw - leaves it false, which is the honest answer:
/// nothing in that close says who asked for it. It lives at file scope because the control that
/// sets it (`ConnectionManagerState`) and the handler that reads it (`_DesktopServerPageState`)
/// are different widgets.
bool _cmClosedByOperator = false;
class DesktopServerPage extends StatefulWidget {
const DesktopServerPage({Key? key}) : super(key: key);
@@ -55,7 +63,10 @@ class _DesktopServerPageState extends State<DesktopServerPage>
@override
void onWindowClose() {
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
// Other platforms keep the old behaviour exactly: the ambiguity this guards against is a
// Linux session logout, which closes every window in the session.
final byOperator = _cmClosedByOperator || !isLinux;
Future.wait([gFFI.serverModel.closeAll(byOperator: byOperator), gFFI.close()]).then((_) {
if (isMacOS) {
RdPlatformChannel.instance.terminate();
} else {
@@ -327,6 +338,7 @@ class ConnectionManagerState extends State<ConnectionManager>
var tabController = gFFI.serverModel.tabController;
final connLength = tabController.length;
if (connLength <= 1) {
_cmClosedByOperator = true;
windowManager.close();
return true;
} else {
@@ -338,6 +350,9 @@ class ConnectionManagerState extends State<ConnectionManager>
res = await closeConfirmDialog();
}
if (res) {
// After the dialog, never before it: an external close while it is open must not
// inherit an intent the user had not expressed yet.
_cmClosedByOperator = true;
windowManager.close();
}
return res;

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:xterm/xterm.dart';
import 'package:flutter_hbb/models/terminal_mouse_handler.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 TerminalView(
return TerminalMouseInteraction(
_terminalModel.terminal,
controller: _terminalModel.terminalController,
focusNode: _terminalFocusNode,

View File

@@ -127,7 +127,6 @@ 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);

View File

@@ -9,9 +9,6 @@ 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';
@@ -1336,6 +1333,12 @@ 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,
@@ -1346,9 +1349,18 @@ 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(),
future: isWindowCanBeAdjusted(context),
hasData: (data) {
final visible = data as bool;
if (!visible) return Offstage();
@@ -1364,36 +1376,201 @@ class ScreenAdjustor {
});
}
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.
// 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.
// 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 +
CanvasModel.leftToEdge +
CanvasModel.rightToEdge) *
horizontalEdges) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
CanvasModel.topToEdge +
CanvasModel.bottomToEdge) *
scale +
magicHeight;
final height =
(canvasModel.getDisplayHeight() * canvasModel.scale + verticalEdges) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
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;
}
if (left < frameRect.left) {
left = frameRect.left;
@@ -1407,69 +1584,101 @@ class ScreenAdjustor {
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, 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;
}
stateGlobal.setMaximized(false);
}
}
updateScreen() async {
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']);
_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;
}
}
_getScreenInfoDesktop() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
return v.result;
}
Future<bool> isWindowCanBeAdjusted() async {
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;
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) {
return false;
}
if (!isWeb) {
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
await updateScreen();
if (_screen == null) {
return false;
}
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);
return await _getAdjustedWindowFrame(mediaSize, forMenu: true) != null;
}
}
@@ -1478,20 +1687,11 @@ class _DisplayMenu extends StatefulWidget {
final FFI ffi;
final ToolbarState state;
final Function(bool) setFullscreen;
final Widget pluginItem;
_DisplayMenu(
{Key? key,
required this.id,
const _DisplayMenu(
{required this.id,
required this.ffi,
required this.state,
required this.setFullscreen})
: pluginItem = LocationItem.createLocationItem(
id,
ffi,
kLocationClientRemoteToolbarDisplay,
true,
),
super(key: key);
required this.setFullscreen});
@override
State<_DisplayMenu> createState() => _DisplayMenuState();
@@ -1529,7 +1729,6 @@ 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),
@@ -1582,9 +1781,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
]);
}
}
if (ffi.connType == ConnType.defaultConn) {
menuChildren.add(widget.pluginItem);
}
return menuChildren;
}
@@ -2096,15 +2292,19 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
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');
}
}
@@ -2176,8 +2376,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
return;
}
if (w == rect.width.toInt() && h == rect.height.toInt()) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context);
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();
}
}
});

View File

@@ -30,9 +30,6 @@ 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;
@@ -141,8 +138,6 @@ 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();
@@ -570,12 +565,6 @@ _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

@@ -366,8 +366,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
return BottomSheetBody(
leading: CircularProgressIndicator(),
title: translate("Waiting"),
text:
"${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s",
text: "${readableFileSize(activeJob.speed)}/s",
onCanceled: () {
model.jobController.cancelJob(activeJob.id);
jobTable.clear();

View File

@@ -1276,6 +1276,14 @@ 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

@@ -46,6 +46,12 @@ class JobID {
typedef GetSessionID = SessionID Function();
typedef GetDialogManager = OverlayDialogManager? Function();
typedef ReadRemoteDirectory = Future<void> Function(
SessionID sessionId, String path, bool includeHidden);
const _kRemoteReadDirTimeout = Duration(seconds: 30);
const _kRemoteSessionChangedError =
'Remote directory read cancelled because the session changed';
class FileModel {
final WeakReference<FFI> parent;
@@ -84,6 +90,7 @@ class FileModel {
}
Future<void> onReady() async {
fileFetcher.beginRemoteSession();
await evtLoop.onReady();
if (!isWeb) await localController.onReady();
await remoteController.onReady();
@@ -133,7 +140,11 @@ class FileModel {
final id = int.tryParse(evt['id']?.toString() ?? '');
if (id != null) {
final err = evt['err']?.toString() ?? 'Unknown error';
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
if (id == 0) {
fileFetcher.tryCompleteRemoteTaskWithError(err);
} else {
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
}
}
// Always call jobController.jobError(evt) to ensure all error events are processed,
// even if the event does not have a valid job ID. This allows for generic error handling
@@ -350,6 +361,8 @@ class FileController {
final history = RxList<String>.empty(growable: true);
final sortBy = SortBy.name.obs;
var sortAscending = true;
// Incremented for each navigation; only the latest generation applies results.
int _directoryRequestGeneration = 0;
final JobController jobController;
final WeakReference<FFI> rootState;
@@ -484,12 +497,19 @@ class FileController {
path = "$path\\";
}
}
final requestGeneration = ++_directoryRequestGeneration;
try {
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
if (requestGeneration != _directoryRequestGeneration) {
return true;
}
fd.format(isWindows, sort: sortBy.value);
directory.value = fd;
return true;
} catch (e) {
if (requestGeneration != _directoryRequestGeneration) {
return true;
}
debugPrint("Failed to openDirectory $path: $e");
return false;
}
@@ -541,6 +561,7 @@ class FileController {
void initDirAndHome(Map<String, dynamic> evt) {
try {
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
final isHomeResponse = fileFetcher.isLikelyRemoteHomeResponse(fd.path);
fd.format(options.value.isWindows, sort: sortBy.value);
if (fd.id > 0) {
final jobIndex = jobController.getJob(fd.id);
@@ -556,10 +577,12 @@ class FileController {
debugPrint("update receive details: ${fd.path}");
jobController.jobTable.refresh();
}
} else if (options.value.home.isEmpty) {
} else if (options.value.home.isEmpty && isHomeResponse) {
options.value.home = fd.path;
debugPrint("init remote home: ${fd.path}");
directory.value = fd;
if (_directoryRequestGeneration == 0) {
directory.value = fd;
}
}
} catch (e) {
debugPrint("initDirAndHome err=$e");
@@ -1362,16 +1385,78 @@ class JobResultListener<T> {
}
}
class _RemoteReadTask {
final bool includeHidden;
final Completer<FileDirectory> completer = Completer<FileDirectory>();
final Completer<void> released = Completer<void>();
late final Timer timer;
_RemoteReadTask(this.includeHidden);
}
class FileFetcher {
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
Map<String, Completer<FileDirectory>> remoteTasks = {};
final Map<String, _RemoteReadTask> _remoteReadTasks = {};
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
int _remoteSessionGeneration = 0;
final GetSessionID getSessionID;
final ReadRemoteDirectory _readRemoteDirectory;
SessionID get sessionId => getSessionID();
FileFetcher(this.getSessionID);
FileFetcher(this.getSessionID, {ReadRemoteDirectory? readRemoteDirectory})
: _readRemoteDirectory = readRemoteDirectory ??
((sessionId, path, includeHidden) => bind.sessionReadRemoteDir(
sessionId: sessionId,
path: path,
includeHidden: includeHidden));
bool hasPendingRemoteRead(String path) => _remoteReadTasks.containsKey(path);
bool isLikelyRemoteHomeResponse(String path) =>
_remoteReadTasks.isEmpty ||
(_remoteReadTasks.length == 1 &&
hasPendingRemoteRead("") &&
!hasPendingRemoteRead(path));
void beginRemoteSession() {
_remoteSessionGeneration++;
final pendingTasks = _remoteReadTasks.entries.toList(growable: false);
for (final entry in pendingTasks) {
final task = entry.value;
if (!_removeRemoteReadTask(entry.key, task)) continue;
task.completer.completeError(StateError(_kRemoteSessionChangedError));
}
}
_RemoteReadTask _registerRemoteReadTask(String path, bool includeHidden) {
if (hasPendingRemoteRead(path)) {
throw "Failed to registerReadTask, already have same read job";
}
final task = _RemoteReadTask(includeHidden);
_remoteReadTasks[path] = task;
task.timer = Timer(_kRemoteReadDirTimeout, () {
if (!_removeRemoteReadTask(path, task)) return;
task.completer.completeError("Failed to read dir, timeout");
});
return task;
}
bool _removeRemoteReadTask(String path, _RemoteReadTask task) {
if (!identical(_remoteReadTasks[path], task)) return false;
_remoteReadTasks.remove(path);
task.timer.cancel();
task.released.complete();
return true;
}
bool _completeRemoteReadTask(String path, FileDirectory directory) {
final task = _remoteReadTasks[path];
if (task == null || !_removeRemoteReadTask(path, task)) return false;
task.completer.complete(directory);
return true;
}
Future<List<FileDirectory>> registerReadEmptyDirsTask(
bool isLocal, String path) {
@@ -1391,23 +1476,6 @@ class FileFetcher {
return c.future;
}
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteTasks; // bypass now
if (tasks.containsKey(path)) {
throw "Failed to registerReadTask, already have same read job";
}
final c = Completer<FileDirectory>();
tasks[path] = c;
Timer(Duration(seconds: 2), () {
tasks.remove(path);
if (c.isCompleted) return;
c.completeError("Failed to read dir, timeout");
});
return c.future;
}
Future<FileDirectory> registerReadRecursiveTask(int actID) {
final tasks = readRecursiveTasks;
if (tasks.containsKey(actID)) {
@@ -1445,27 +1513,37 @@ class FileFetcher {
tryCompleteTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return;
late final Map<Object, Completer<FileDirectory>> tasks;
try {
final fd = FileDirectory.fromJson(jsonDecode(msg));
if (fd.id > 0) {
// fd.id > 0 is result for read recursive
// to-do later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
tasks = readRecursiveTasks;
final completer = tasks.remove(fd.id);
completer?.complete(fd);
} else if (fd.path.isNotEmpty) {
// result for normal read dir
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
tasks = remoteTasks; // bypass now
final completer = tasks.remove(fd.path);
final completer = readRecursiveTasks.remove(fd.id);
completer?.complete(fd);
return;
}
if (isLocalStr == "false" && fd.path.isNotEmpty) {
if (_completeRemoteReadTask(fd.path, fd)) {
return;
}
// A Home request uses an empty path but returns its resolved path.
if (isLikelyRemoteHomeResponse(fd.path)) {
_completeRemoteReadTask("", fd);
}
}
} catch (e) {
debugPrint("tryCompleteJob err: $e");
}
}
bool tryCompleteRemoteTaskWithError(String error) {
if (_remoteReadTasks.length != 1) return false;
final entry = _remoteReadTasks.entries.single;
final task = entry.value;
if (!_removeRemoteReadTask(entry.key, task)) return false;
task.completer.completeError(error);
return true;
}
// Complete a pending recursive read task with an error.
// See FileModel.handleJobError() for why this is necessary.
void tryCompleteRecursiveTaskWithError(int id, String error) {
@@ -1506,9 +1584,26 @@ class FileFetcher {
final fd = FileDirectory.fromJson(jsonDecode(res));
return fd;
} else {
await bind.sessionReadRemoteDir(
sessionId: sessionId, path: path, includeHidden: showHidden);
return registerReadTask(isLocal, path);
final remoteSessionGeneration = _remoteSessionGeneration;
final pendingTask = _remoteReadTasks[path];
if (pendingTask != null) {
if (pendingTask.includeHidden == showHidden) {
return pendingTask.completer.future;
}
await pendingTask.released.future;
if (remoteSessionGeneration != _remoteSessionGeneration) {
throw StateError(_kRemoteSessionChangedError);
}
return fetchDirectory(path, isLocal, showHidden);
}
final task = _registerRemoteReadTask(path, showHidden);
unawaited(Future<void>.sync(
() => _readRemoteDirectory(sessionId, path, showHidden))
.catchError((Object error, StackTrace stackTrace) {
if (!_removeRemoteReadTask(path, task)) return;
task.completer.completeError(error, stackTrace);
}));
return task.completer.future;
}
} catch (e) {
return Future.error(e);

View File

@@ -1787,6 +1787,11 @@ 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,9 +25,6 @@ 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;
@@ -437,15 +434,6 @@ 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'];
@@ -920,17 +908,12 @@ 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', false);
sessionId, dialogManager, 'terminal-admin-login-tip');
} else if (type == 'terminal-admin-login-password') {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'terminal-admin-login-tip', false);
sessionId, dialogManager, 'terminal-admin-login-tip');
} 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
@@ -2225,6 +2208,7 @@ 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
@@ -2273,12 +2257,19 @@ 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) {
@@ -2507,6 +2498,7 @@ 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.
@@ -2739,6 +2731,7 @@ class CanvasModel with ChangeNotifier {
_x = 0;
_y = 0;
_scale = 1.0;
_locked = false;
_lastViewStyle = ViewStyle.defaultViewStyle();
_timerMobileFocusCanvasCursor?.cancel();
_timerMobileRestoreCanvasOffset?.cancel();
@@ -2850,7 +2843,7 @@ class CursorData {
required this.width,
required this.height,
}) : hotx = hotxOrigin * scale,
hoty = hotxOrigin * scale;
hoty = hotyOrigin * scale;
int _doubleToInt(double v) => (v * 10e6).round().toInt();
@@ -4171,7 +4164,6 @@ 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

@@ -738,9 +738,13 @@ class ServerModel with ChangeNotifier {
}
}
Future<void> closeAll() async {
await Future.wait(
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
/// `byOperator` false means the CM's window went away rather than a person asking for the
/// peers to go. The sessions end either way; only the close reason differs, and with it
/// whether the peer is allowed to reconnect. See `ipc::Data::CmWindowClosed`.
Future<void> closeAll({bool byOperator = true}) async {
await Future.wait(_clients.map((client) => byOperator
? bind.cmCloseConnection(connId: client.id)
: bind.cmCloseConnectionWindow(connId: client.id)));
_clients.clear();
tabController.state.value.tabs.clear();
if (isAndroid) androidUpdatekeepScreenOn();

View File

@@ -0,0 +1,66 @@
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

@@ -0,0 +1,235 @@
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,10 +1,19 @@
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();
const WheelButtonFixMouseHandler({this.positionProvider});
final CellOffset? Function()? positionProvider;
@override
String? call(TerminalMouseEvent event) {
@@ -23,20 +32,270 @@ 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 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';
}
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,
),
);
}
}

View File

@@ -10,8 +10,6 @@ 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

@@ -1,42 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,79 +0,0 @@
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

@@ -1,319 +0,0 @@
// 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

@@ -1,110 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,86 +0,0 @@
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

@@ -1,301 +0,0 @@
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

@@ -1,202 +0,0 @@
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,10 +762,6 @@ 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', [
@@ -1377,6 +1373,10 @@ class RustdeskImpl {
throw UnimplementedError("cmLoginRes");
}
Future<void> cmCloseConnectionWindow({required int connId, dynamic hint}) {
throw UnimplementedError("cmCloseConnectionWindow");
}
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
throw UnimplementedError("cmCloseConnection");
}
@@ -1644,78 +1644,6 @@ 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,5 +1,4 @@
import 'dart:js' as js;
import 'dart:html' as html;
// cycle imports, maybe we can improve this
import 'package:flutter_hbb/consts.dart';
@@ -13,8 +12,6 @@ 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

@@ -1,14 +0,0 @@
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,8 +36,28 @@ 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
@@ -278,6 +298,16 @@ 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

@@ -340,7 +340,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: 533883bcb0ffe91a9afdb13b8bac9b14b3e054ba
resolved-ref: 8b774a66671cbb9bcb2631af6ac28f9bdd469ce3
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -529,11 +529,12 @@ packages:
flutter_custom_cursor:
dependency: "direct main"
description:
name: flutter_custom_cursor
sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
url: "https://pub.dev"
source: hosted
version: "0.0.4"
path: "."
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
resolved-ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
url: "https://github.com/rustdesk-org/flutter_custom_cursor"
source: git
version: "0.0.3"
flutter_gpu_texture_renderer:
dependency: "direct main"
description:
@@ -1597,9 +1598,9 @@ packages:
dependency: "direct main"
description:
path: "plugins/window_size"
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
url: "https://github.com/google/flutter-desktop-embedding.git"
ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
resolved-ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
url: "https://github.com/21pages/flutter-desktop-embedding.git"
source: git
version: "0.1.0"
xdg_directories:

View File

@@ -58,12 +58,15 @@ dependencies:
git:
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
freezed_annotation: ^2.0.3
flutter_custom_cursor: ^0.0.4
flutter_custom_cursor:
git:
url: https://github.com/rustdesk-org/flutter_custom_cursor
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
url: https://github.com/21pages/flutter-desktop-embedding.git
path: plugins/window_size
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
ref: 51e67ce047c72b26810b99e8473ddb44612fe356
get: ^4.6.5
visibility_detector: ^0.4.0+2
contextmenu: ^3.0.0

View File

@@ -0,0 +1,281 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:uuid/uuid.dart';
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
class _FakeFFI implements FFI {
@override
String id = 'test-peer';
@override
UuidValue get sessionId => _sessionId;
@override
late final FfiModel ffiModel = FfiModel(WeakReference(this));
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
FileController _createController(FileFetcher fileFetcher) {
final ffi = _FakeFFI();
return FileController(
isLocal: false,
getSessionID: () => _sessionId,
rootState: WeakReference(ffi),
jobController: JobController(() => _sessionId, () => null),
fileFetcher: fileFetcher,
getOtherSideDirectoryData: () =>
DirectoryData(FileDirectory(), DirectoryOptions()),
);
}
FileDirectory _directory(String path) => FileDirectory()..path = path;
String _directoryJson(String path) => jsonEncode({
'id': 0,
'path': path,
'entries': <Object>[],
});
class _SentRead {
final String path;
final bool includeHidden;
const _SentRead(this.path, this.includeHidden);
}
void main() {
test('a fast remote response is matched after registration', () async {
late final FileFetcher fileFetcher;
fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, __) {
fileFetcher.tryCompleteTask(_directoryJson(path), 'false');
return Future<void>.value();
},
);
final directory = await fileFetcher.fetchDirectory('/fast', false, false);
expect(directory.path, '/fast');
});
test('a send failure fails and removes its registered task', () async {
final failure = StateError('send failed');
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) => Future<void>.error(failure),
);
await expectLater(
fileFetcher.fetchDirectory('/failed', false, false),
throwsA(same(failure)),
);
expect(fileFetcher.hasPendingRemoteRead('/failed'), isFalse);
});
test('a resolved Home path completes the sole empty-path request', () async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final home = controller.openDirectory('');
await Future<void>.delayed(Duration.zero);
final response = _directoryJson('/home/user');
controller.initDirAndHome({'value': response});
expect(controller.homePath, '/home/user');
expect(controller.directory.value.path, '/initial');
fileFetcher.tryCompleteTask(response, 'false');
expect(await home, isTrue);
expect(controller.directory.value.path, '/home/user');
expect(sent.single.path, isEmpty);
});
test('an automatic response initializes Home without a pending request', () {
final controller = _createController(FileFetcher(() => _sessionId));
controller.initDirAndHome({'value': _directoryJson('/home/user')});
expect(controller.homePath, '/home/user');
expect(controller.directory.value.path, '/home/user');
});
test('an exact path response is not taken by a pending Home request',
() async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final home = fileFetcher.fetchDirectory('', false, false);
final regular = fileFetcher.fetchDirectory('/regular', false, false);
await Future<void>.delayed(Duration.zero);
var homeCompleted = false;
home.then<void>((_) => homeCompleted = true);
fileFetcher.tryCompleteTask(_directoryJson('/unmatched'), 'false');
await Future<void>.delayed(Duration.zero);
expect(homeCompleted, isFalse);
fileFetcher.tryCompleteTask(_directoryJson('/regular'), 'false');
expect((await regular).path, '/regular');
await Future<void>.delayed(Duration.zero);
expect(homeCompleted, isFalse);
fileFetcher.tryCompleteTask(_directoryJson('/home/user'), 'false');
expect((await home).path, '/home/user');
expect(sent.map((request) => request.path), ['', '/regular']);
});
test('a read error completes the sole pending request', () async {
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) async {},
);
final request = fileFetcher.fetchDirectory('/denied', false, false);
await Future<void>.delayed(Duration.zero);
final expectation = expectLater(request, throwsA('permission denied'));
fileFetcher.tryCompleteRemoteTaskWithError('permission denied');
await expectation;
expect(fileFetcher.hasPendingRemoteRead('/denied'), isFalse);
});
test('same-path requests share the pending read', () async {
final sent = <_SentRead>[];
late final FileFetcher fileFetcher;
fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final first = controller.openDirectory('/same');
final waiting = controller.openDirectory('/same');
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.path), ['/same']);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect(await first, isTrue);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.path), ['/same']);
expect(await waiting, isTrue);
expect(controller.directory.value.path, '/same');
});
test('same-path requests with different hidden options are serialized',
() async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final first = fileFetcher.fetchDirectory('/same', false, false);
final second = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await first).path, '/same');
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false, true]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await second).path, '/same');
});
test('session invalidation cancels active and waiting reads', () async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final first = fileFetcher.fetchDirectory('/same', false, false);
final waiting = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
final firstError = expectLater(first, throwsA(isA<StateError>()));
final waitingError = expectLater(waiting, throwsA(isA<StateError>()));
fileFetcher.beginRemoteSession();
await firstError;
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false]);
await waitingError;
expect(fileFetcher.hasPendingRemoteRead('/same'), isFalse);
final replacement = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false, true]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await replacement).path, '/same');
});
test('a late dispatch failure cannot remove a replacement task', () async {
final dispatches = <Completer<void>>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) {
final dispatch = Completer<void>();
dispatches.add(dispatch);
return dispatch.future;
},
);
final first = fileFetcher.fetchDirectory('/same', false, false);
await Future<void>.delayed(Duration.zero);
final firstError = expectLater(first, throwsA(isA<StateError>()));
fileFetcher.beginRemoteSession();
await firstError;
final replacement = fileFetcher.fetchDirectory('/same', false, false);
await Future<void>.delayed(Duration.zero);
expect(dispatches, hasLength(2));
dispatches.first.completeError(StateError('late dispatch failure'));
await Future<void>.delayed(Duration.zero);
expect(fileFetcher.hasPendingRemoteRead('/same'), isTrue);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await replacement).path, '/same');
dispatches.last.complete();
await Future<void>.delayed(Duration.zero);
});
test('navigation ignores stale directory responses', () async {
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) async {},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final stale = controller.openDirectory('/stale');
final latest = controller.openDirectory('/latest');
await Future<void>.delayed(Duration.zero);
fileFetcher.tryCompleteTask(_directoryJson('/latest'), 'false');
expect(await latest, isTrue);
fileFetcher.tryCompleteTask(_directoryJson('/stale'), 'false');
expect(await stale, isTrue);
expect(controller.directory.value.path, '/latest');
});
}

View File

@@ -1,7 +1,35 @@
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;
@@ -12,6 +40,34 @@ 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,
@@ -111,4 +167,133 @@ 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,6 +42,13 @@ 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 {
@@ -390,3 +397,52 @@ 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.2). We deliberately do
# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.4). 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"]
drm = ["wayland", "hbb_common/wayland_probe"]
mediacodec = ["ndk"]
linux-pkg-config = ["dep:pkg-config"]
hwcodec = ["dep:hwcodec"]

View File

@@ -48,6 +48,7 @@ pub struct Capturer {
duplication: ComPtr<IDXGIOutputDuplication>,
fastlane: bool,
surface: ComPtr<IDXGISurface>,
readable: ComPtr<ID3D11Texture2D>,
texture: ComPtr<ID3D11Texture2D>,
width: usize,
height: usize,
@@ -163,6 +164,7 @@ 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,
@@ -346,19 +348,19 @@ impl Capturer {
if self.fastlane {
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
} else {
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
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<*mut IDXGISurface> {
unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<()> {
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
(*frame).QueryInterface(
wrap_hresult((*frame).QueryInterface(
&IID_ID3D11Texture2D,
&mut texture as *mut *mut _ as *mut *mut _,
);
))?;
let texture = ComPtr(texture);
#[allow(invalid_value)]
@@ -370,24 +372,37 @@ impl Capturer {
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
texture_desc.MiscFlags = 0;
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);
// Avoid per-frame staging texture allocation and the kernel allocation churn it causes.
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 surface = ptr::null_mut();
(*readable.0).QueryInterface(
&IID_IDXGISurface,
&mut surface as *mut *mut _ as *mut *mut _,
);
let mut surface = ptr::null_mut();
wrap_hresult((*readable.0).QueryInterface(
&IID_IDXGISurface,
&mut surface as *mut *mut _ as *mut *mut _,
))?;
(*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _);
self.readable = readable;
self.surface = ComPtr(surface);
}
Ok(surface)
(*self.context.0).CopyResource(self.readable.0 as *mut _, texture.0 as *mut _);
Ok(())
}
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
@@ -485,10 +500,10 @@ impl Capturer {
}
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
(*frame.0).QueryInterface(
wrap_hresult((*frame.0).QueryInterface(
&IID_ID3D11Texture2D,
&mut texture as *mut *mut _ as *mut *mut _,
);
))?;
let texture = ComPtr(texture);
self.texture = texture;

View File

@@ -19,6 +19,18 @@ 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>,
@@ -171,11 +183,76 @@ 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 get_wayland_displays() {
None => match enumerate_displays() {
Ok(displays) => {
let mut primary_index = None;
if let Some(name) = get_primary_monitor() {
@@ -201,8 +278,9 @@ pub fn get_displays() -> Arc<Displays> {
*lock = Some(displays.clone());
displays
}
Err(err) => {
warn!("Failed to get wayland displays: {}", err);
Err(_err) => {
#[cfg(not(feature = "drm"))]
warn!("Failed to get wayland displays: {}", _err);
Arc::new(Displays {
primary: 0,
displays: Vec::new(),
@@ -215,6 +293,8 @@ 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)
@@ -223,17 +303,21 @@ 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, 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
// 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
pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<DisplayRect>)> {
match get_wayland_displays() {
#[cfg(feature = "drm")]
if backed_off() {
return None;
}
match enumerate_displays() {
Ok(displays) => {
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
}
Err(err) => {
warn!("Failed to get wayland displays: {}", err);
Err(_err) => {
#[cfg(not(feature = "drm"))]
warn!("Failed to get wayland displays: {}", _err);
None
}
}
@@ -386,6 +470,40 @@ 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' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'gst-plugins-base' 'gst-plugin-pipewire')
makedepends=()
checkdepends=()
optdepends=()

View File

@@ -5,6 +5,23 @@
<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)">
@@ -40,17 +57,29 @@
</RegistryKey>
</Component>
<!--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)" >
<Component Id="Product.Registry.InstallState" Guid="E310003A-C292-4851-AE20-7A9B186060CE">
<RegistryKey Root="HKLM" Key="Software\$(var.Product)\InstallState\$(var.Product)" ForceDeleteOnUninstall="yes">
<RegistryValue Type="string" Name="BuildDate" Value="$(var.BuildDate)" />
<RegistryValue Type="string" Name="share_rdp" Value="" />
<!--$ArpStart$-->
<!--$ArpEnd$-->
<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$-->
</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,10 +27,12 @@
<!--$ArpStart$-->
<!--$ArpEnd$-->
<SetProperty Action="SetArpInstallLocation" Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER_INNER]" After="CostFinalize" Sequence="execute" />
<!--$CustomClientPropsStart$-->
<!--$CustomClientPropsEnd$-->
<Property Id="APP_WINDOWS_INSTALLER">
<Property Id="APP_WINDOWS_INSTALLER" Secure="yes">
<RegistrySearch Id="AppWindowsInstallerFolderSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" />
</Property>
</Fragment>

View File

@@ -21,8 +21,6 @@ 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." />
@@ -35,8 +33,6 @@ 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,6 +14,7 @@
<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" />
@@ -22,10 +23,11 @@
<UIRef Id="WixUI_ErrorProgressText" />
<InstallUISequence>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND APP_WINDOWS_INSTALLER=&quot;#0&quot;"/>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER=&quot;#0&quot; OR APP_WINDOWS_INSTALLER32=&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-->
@@ -45,7 +47,9 @@
<ComponentRef Id="Product.Registry.CommandPlay" />
<ComponentRef Id="Product.Registry.URLProtocol" />
<ComponentRef Id="Product.Registry.Command" />
<ComponentRef Id="Product.Registry.UninstallApp" />
<ComponentRef Id="Product.Registry.InstallState" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall64" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall32" />
<ComponentRef Id="App.StartMenu" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" />

View File

@@ -12,6 +12,7 @@ import platform
from pathlib import Path
from itertools import chain
import shutil
from xml.sax.saxutils import quoteattr
g_indent_unit = "\t"
g_version = ""
@@ -54,14 +55,14 @@ def make_parser():
parser.add_argument(
"--arp",
action="store_true",
help="Is ARPSYSTEMCOMPONENT",
help="Deprecated; native MSI ARP registration is always used.",
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
@@ -258,25 +259,19 @@ def gen_custom_dialog_bitmaps():
)
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
def gen_native_arp_properties():
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="{v["msi"]}" Value="{v["v"]}" />\n'
f'{indent}<Property Id={quoteattr(str(v["msi"]))} '
f'Value={quoteattr(str(v["v"]))} />\n'
)
for i, line in enumerate(lines_new):
@@ -291,94 +286,16 @@ def gen_custom_ARPSYSTEMCOMPONENT_False(args):
)
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 gen_install_state_values():
def func(lines, index_start):
indent = g_indent_unit * 5
lines_new = []
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"
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}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
f'{indent}<RegistryValue Type={quoteattr(str(value_type))} '
f'Name={quoteattr(str(name))} Value={quoteattr(str(value["v"]))} />\n'
)
for i, line in enumerate(lines_new):
@@ -387,24 +304,35 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
return gen_content_between_tags(
"Package/Components/Regs.wxs",
"<!--$ArpStart$-->",
"<!--$ArpEnd$-->",
"<!--$InstallStateStart$-->",
"<!--$InstallStateEnd$-->",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
def gen_custom_ARPSYSTEMCOMPONENT(args, _dist_dir):
try:
custom_arp = json.loads(args.custom_arp)
g_arpsystemcomponent.update(custom_arp)
except json.JSONDecodeError as e:
custom_arp = dict(json.loads(args.custom_arp))
except (json.JSONDecodeError, TypeError, ValueError) as e:
print(f"Failed to decode custom arp: {e}")
return False
if args.arp:
return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
else:
return gen_custom_ARPSYSTEMCOMPONENT_False(args)
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()
def gen_conn_type(args):
def func(lines, index_start):

View File

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

View File

@@ -1,5 +0,0 @@
#%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 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 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 pam gstreamer1-plugins-base
Requires: gtk3 libxcb libXfixes alsa-lib libva 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 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 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 pam gstreamer1-plugins-base
Requires: gtk3 libxcb libXfixes alsa-lib libva2 gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3 libxdo
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/

View File

@@ -1,130 +0,0 @@
#!/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

View File

@@ -0,0 +1,13 @@
diff --git a/build/cmake/aom_configure.cmake b/build/cmake/aom_configure.cmake
index aaef2c310..5500ad4a3 100644
--- a/build/cmake/aom_configure.cmake
+++ b/build/cmake/aom_configure.cmake
@@ -309,6 +309,8 @@ if(MSVC)
# Disable MSVC warnings that suggest making code non-portable.
add_compiler_flag_if_supported("/wd4996")
+ # Disable MSVC warnings for potentially uninitialized local pointer variable.
+ add_compiler_flag_if_supported("/wd4703")
if(ENABLE_WERROR)
add_compiler_flag_if_supported("/WX")
endif()

View File

@@ -1,7 +1,7 @@
diff --git a/build/cmake/aom_configure.cmake b/build/cmake/aom_configure.cmake
diff --git a/cmake/aom_configure.cmake b/cmake/aom_configure.cmake
index aaef2c310..5500ad4a3 100644
--- a/build/cmake/aom_configure.cmake
+++ b/build/cmake/aom_configure.cmake
--- a/cmake/aom_configure.cmake
+++ b/cmake/aom_configure.cmake
@@ -309,6 +309,8 @@ if(MSVC)
# Disable MSVC warnings that suggest making code non-portable.

View File

@@ -9,25 +9,24 @@ get_filename_component(PERL_PATH ${PERL} DIRECTORY)
vcpkg_add_to_path(${PERL_PATH})
if(DEFINED ENV{USE_AOM_391})
set(AOM_CONFIG_PATH "lib/cmake/aom")
vcpkg_from_git(
OUT_SOURCE_PATH SOURCE_PATH
URL "https://aomedia.googlesource.com/aom"
REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1
PATCHES
aom-uninitialized-pointer.diff
aom-uninitialized-pointer-3.9.1.diff
aom-avx2.diff
aom-install.diff
)
else()
set(AOM_CONFIG_PATH "lib/cmake/AOM")
vcpkg_from_git(
OUT_SOURCE_PATH SOURCE_PATH
URL "https://aomedia.googlesource.com/aom"
REF 10aece4157eb79315da205f39e19bf6ab3ee30d0 # 3.12.1
REF 03087864cf4bea6abb0d28f95cf7843511413d8f # 3.14.1
PATCHES
aom-uninitialized-pointer.diff
# aom-avx2.diff
# Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream
aom-install.diff
)
endif()
@@ -67,7 +66,7 @@ if(VCPKG_TARGET_IS_WINDOWS)
endif()
# Move cmake configs
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT})
vcpkg_cmake_config_fixup(CONFIG_PATH ${AOM_CONFIG_PATH})
# Remove duplicate files
file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include

View File

@@ -1,6 +1,6 @@
{
"name": "aom",
"version-semver": "3.12.1",
"version-semver": "3.14.1",
"port-version": 0,
"description": "AV1 codec library",
"homepage": "https://aomedia.googlesource.com/aom",

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