Compare commits

...

36 Commits

Author SHA1 Message Date
fufesou 3c574a4182 fix(wayland): clipboard, support ext-data-control (#15366)
* fix(wayland): clipboard, support ext-data-control

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

* fix(clipboard): restart stale listener and log join panics

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

* update clipboard-master

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

* refactor(clipboard): remove redundant stale listener cleanup

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-21 17:09:41 +08:00
Tigah 311d4708e5 fix(linux): reap a crashed headless session's leftovers on next start (#15348)
The teardown cleanup added for #15183 only runs on a clean disconnect.
If the service or its --server crashes before then, the headless logind
session scope and the /tmp/.X<n> lock files it created leak the same way
#15183 leaked them, with nothing to reclaim them afterwards.

Record the session scope and display when the headless session starts,
and on the next --server start reap exactly what the previous run
recorded, then drop the marker. It only ever touches the one scope and
display the previous run recorded, never a scan, so unrelated sessions
are untouched; the reap and X cleanup reuse the teardown path.

A logind session id is only unique within a boot: the counter lives in
/run and resets, so a recorded "session-N.scope" can name a different,
live session after a reboot. Tag the marker with the boot id and only
reap the scope when it matches the current boot. A leaked cgroup cannot
outlive a reboot, so nothing legitimate is lost cross-boot; the X lock
cleanup stays pid-guarded and runs either way.

Signed-off-by: TBX3D <88289044+TBX3D@users.noreply.github.com>
2026-06-21 16:56:41 +08:00
Maison da Silva 5cf4323d07 Fix Portuguese translations for consistency (#15354) 2026-06-21 16:52:28 +08:00
bovirus 3976701ac6 Update Italian translations (#15367) 2026-06-21 16:52:13 +08:00
fufesou 9ded8d6ab2 fix(keyboard): win, key, Pause (#15351)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-20 21:11:19 +08:00
rustdesk cd2fff0655 fix hbb_common 2026-06-20 18:31:42 +08:00
rustdesk 10f61ffdc2 translate 2026-06-20 18:27:42 +08:00
Tigah d72952bf93 fix(linux): clean up leftover session procs and X locks on headless teardown (#15337)
On headless login the desktop manager opens a PAM session, which makes
pam_systemd register a logind session and put the spawned Xorg + window
manager and their children (e.g. pipewire) in a "session-<id>.scope"
cgroup. Teardown only killed the Xorg and wm pids, so the rest of the
session kept running, holding the logind session in "closing" and leaking
runtime sockets and X display numbers on every reconnect.

Capture the session scope cgroup from a child pid and, on teardown, kill the
remaining processes in it and any descendant cgroups (cgroup.procs is not
recursive, and a desktop may move pipewire and apps into child scopes),
excluding our own service process and anything tracked in CHILD_PROCESS
together with its descendants. The connection manager is a sudo child, so the
tracked pid is the wrapper while the real --cm-no-ui worker may be a descendant
(sudo with use_pty runs it under a monitor); both can share the scope when
their PAM stack does not re-home them.

Xorg is killed with SIGKILL, so it also leaves its "/tmp/.X<n>-lock" and
"/tmp/.X11-unix/X<n>" behind; get_avail_display() treats either file as the
display being in use, so the number is never reused and climbs until the
range is exhausted. Remove those files for the session's display on
teardown, as a clean Xorg exit would.

Closes #15183

Signed-off-by: TBX3D <88289044+TBX3D@users.noreply.github.com>
2026-06-20 14:21:42 +08:00
Re*Index. (ot_inc) a7c55db9ac Fix Japanese text (#15345) 2026-06-20 09:50:04 +08:00
StealUrKill bff47e2b81 Feature: Add monitor-switch buttons to remote toolbars (#15342)
* Feature: add monitor-switch buttons to remote toolbars

Add buttons to cycle through the remote displays from the toolbars:

- A main-toolbar button and a minimized-handle button, both using a shared SVG icon with the current monitor number overlaid.
- Two opt-in settings under Settings/Other. The minimized-toolbar option is nested under the main-toolbar option.
- The minimized button only appears once the toolbar is collapsed.
- Cycling does not move the remote cursor, matching the existing in-toolbar monitor buttons.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>

* fix: respect individual-window display mode when cycling

In "Show displays as individual windows" mode, route the cycle button through openMonitorInNewTabOrWindow like the monitor selector, so each display keeps its own window instead of repurposing the current one.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>

---------

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
2026-06-20 01:22:20 +08:00
fufesou 3d478c4935 fix(ios): mouse mismatch (#15339)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-19 20:50:56 +08:00
RustDesk a658e987b7 Revert "Feature: Add monitor-switch buttons to remote toolbars (#15314)" (#15338)
This reverts commit 7c8b0adc1e.
2026-06-19 12:34:59 +08:00
StealUrKill 7c8b0adc1e Feature: Add monitor-switch buttons to remote toolbars (#15314)
* Feature: add monitor-switch buttons to remote toolbars

Add a one-click "switch to next monitor" control to both desktop toolbars:
- Main toolbar: always shown when the remote has more than one monitor,
  styled to match the existing blue icon buttons (white screen, black number).
- Minimized (draggable show/hide) toolbar: off by default, toggled via a new
  "Show monitor switch on minimized toolbar" checkbox in the Display menu and
  persisted as a local option.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>

* Update remote_toolbar.dart

* refact: unify monitor-switch button icons, share tooltip

Addressing the review feedback on the monitor-switch toolbar buttons:

- Add assets/display_switcher.svg and use it for both the main and minimized buttons. This replaces the hand-drawn glyph (Containers + magic numbers) on the main toolbar. The icon scales with DPI/theme and the two toolbars stay visually consistent.
- Flip the minimized button's label to white for contrast, since the new icon has a solid screen.
- Move the tooltip string into a shared _MonitorCycle.tooltip getter so both buttons use one source of truth.
- Use const Offstage() for consistency with the surrounding returns.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>

* Improve monitor-switch settings and toolbar behavior

- Nest the minimized-toolbar option under the main one in settings only show when the main option is enabled.

- Only show the minimized switch button on the collapsed toolbar handle, so it no longer duplicates the main switch while the toolbar is expanded.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>

---------

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-06-19 11:19:08 +08:00
fufesou a732ebc3e1 fix: win, ci, update RustDeskTempTopMostWindow (#15334)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-19 00:24:48 +08:00
fufesou 30c0867e40 fix(ci): win arm64 (#15333)
* fix(ci): win arm64

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

* fix: ci, less changes

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-19 00:10:37 +08:00
Dennis Ameling 8f50ea64dc Add Windows arm64 support (#15139)
* Add initial arm64 build logic

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Upgrade Flutter to 3.44.0 and introduce Windows arm64 in CI

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Bump bridge build to Flutter 3.44 as well

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Fix install flutter step for Win arm64

* Bump install-llvm-action to v2 for arm64 support

* Fix libsodium logic to only install through vcpkg on win arm64

* Fix Flutter installations on Win

* Flutter XCode: only build the current arch as it defaults to universal

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Ensure that we really have arm64 Dart + Flutter engine in CI

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Enable hwcodec feature now that upstream supports building it

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* CI: improve logic for getting Flutter arm64 SDK

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>

* Apply PR feedback (only bump Flutter version on Win arm64)

* Exclude MSI build on arm64

* CI: build the MSI for Windows arm64 (WiX v4 ARM64 platform + native CustomActions)

* Address PR feedback

* Update Cargo.toml

* Update Cargo.lock

* Update Cargo.lock

* Add Flutter 3.44 DialogThemeData background colors

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

---------

Signed-off-by: Dennis Ameling <dennis@dennisameling.com>
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-06-18 22:37:15 +08:00
fufesou 0797ebb695 Refact/privacy mode 1 multi monitors (#15321)
* refact: privacy mdoe 1, multi-monitors

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

* fix: harden privacy mode overlay & capture cleanup

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

* Fix privacy mode edge cases after multi-monitor overlay changes

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

* Add missing changes

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-18 21:27:44 +08:00
Bia503 c9391fb894 fix(arm64-linux): fix CJK font rendering on flutter-elinux (#15324)
* fix(arm64-linux): fix CJK font rendering on flutter-elinux

The flutter-elinux engine used for ARM64 Linux builds is compiled without
--enable-fontconfig, so Flutter's text shaper cannot discover system fonts.
This causes CJK characters to render as tofu boxes even when fonts such as
Noto Sans CJK are installed. See flutter/flutter#139293.

Fix by loading a CJK font at startup via FontLoader (bypassing fontconfig)
and propagating it through two paths so all text widgets are covered:

1. MyTheme.applyFontFallback() — updates textTheme on both light and dark
   ThemeData so Material components receive the fallback through the theme.

2. _mergeCjkFallback() in GetMaterialApp builders — wraps child widgets in
   DefaultTextStyle.merge so bare Text() widgets and those with inherit:true
   also render CJK characters correctly.

Font discovery queries fc-list for zh, ja, and ko separately, preferring
fonts present in all three sets (true pan-CJK fonts such as NotoSansCJK or
SourceHanSans) over Chinese-only fonts that may lack Japanese kana or Korean
hangul glyphs.  Falls back to a hardcoded search-path list covering
Debian/Ubuntu, Fedora/RHEL, Arch Linux, and WenQuanYi font layouts.

This is an app-level workaround. The engine-level fix is tracked at
flutter/flutter#180235 (open as of 2026-06).

Fixes #10666

Signed-off-by: Bia503 <yinwenche189@gmail.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Bia503 <yinwenche189@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-18 20:47:24 +08:00
Maison da Silva 8a955888bf Fix Portuguese translations for consistency (#15325)
Fix Portuguese translations for consistency
2026-06-18 10:28:34 +08:00
21pages 36e812e550 update hwcodec (#15323)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-17 22:58:46 +08:00
rustdesk 8baa995c7a bump version 2026-06-17 22:18:45 +08:00
RustDesk f4a0535289 autocomplete online (#15313)
* autocomplete online

* review fix

* review fix

* remove literalInput
2026-06-17 22:04:34 +08:00
RustDesk 6665242edf Revert "refact: privacy mdoe 1, multi-monitors (#15318)" (#15320)
This reverts commit 3cdf1cce54.
2026-06-17 21:46:40 +08:00
fufesou 3cdf1cce54 refact: privacy mdoe 1, multi-monitors (#15318)
* refact: privacy mdoe 1, multi-monitors

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

* fix: harden privacy mode overlay & capture cleanup

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-17 21:43:57 +08:00
fufesou 88ae00ba73 refact: restart remote device, autoconnect (#15290)
* refact: restart remote device, autoconnect

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

* fix: guard restart reconnect timer after session close

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

* Simple refactor

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

* fix(restart): auto connect, comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-17 17:50:18 +08:00
Maison da Silva 7c26575dbd Update translation for remote toolbar docking message (#15297)
* Update translation for remote toolbar docking message

Update translation for remote toolbar docking message

* Translate 'Display' to 'Tela' in Portuguese locale

* Change translation of 'Display' to 'Exibição'
2026-06-17 17:36:27 +08:00
fufesou 93d064a9b0 refact(oidc): icon azure to microsoft (#15278)
* refact(oidc): icon azure to microsoft

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

* Simple refactor

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

* refact: oidc, remove unused auth-azure.svg

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-17 14:36:53 +08:00
rustdesk bf206dc309 fixing https://github.com/rustdesk/rustdesk/issues/15293 2026-06-16 11:36:53 +08:00
fufesou 6d116cf1c9 fix(clipboard): Windows DIB images, fill missing alpha (#15296)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-16 11:29:53 +08:00
RustDesk 1fc33218dc Revert "fix(iPad): keep touch gestures with external mouse (#14652)" (#15288)
This reverts commit 5b7ad339b8.
2026-06-15 15:13:54 +08:00
21pages b73e5bbfa0 opt: release clipboard config lock before updates (#15277)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-14 17:27:19 +08:00
fufesou 78533e428e feat: theme logo (#15268)
* feat: theme logo

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

* perf(flutter): cache theme logo asset resolution

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-14 13:47:39 +08:00
fufesou cc7fe4efdc Fix/generate py target injection (#15248)
* fix: generate.py, target injection

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

* refact: logs

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

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update port_forward.rs

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-11 23:09:34 +08:00
littlestejan 84af60c07e Fix clipboard synchronization not fully disabled in View Only mode (#15224)
* fix: view-only clipboard sync

Signed-off-by: Setani <little_stejan@hotmail.com>

* fix: gate Android MultiClipboards handling with clipboard permissions

Signed-off-by: Setani <little_stejan@hotmail.com>

---------

Signed-off-by: Setani <little_stejan@hotmail.com>
2026-06-10 07:42:58 +08:00
fufesou 6426269d41 Refact/printer driver default unchecked (#15191)
* refact: installation, printer driver, default unchecked

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

* refact: silent install, get option from the reg values

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

* refact: silent install, arg printer=[0|1]

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-06 08:51:08 +08:00
dependabot[bot] 7c41f993fe Git submodule: Bump libs/hbb_common from df6badc to 387603f (#15189)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `df6badc` to `387603f`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](https://github.com/rustdesk/hbb_common/compare/df6badca5bf81b4e9836256cf8e31c993ad70dd1...387603f47cbb15c0d3dc3d67ae3396d3eb707daf)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 387603f47cbb15c0d3dc3d67ae3396d3eb707daf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:04:02 +08:00
119 changed files with 2557 additions and 412 deletions
+2
View File
@@ -2,6 +2,8 @@
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Applies the Flutter 3.44-only source/pubspec changes on the fly, in CI only.
#
# Windows arm64 needs Flutter >= 3.44 (the first stable release shipping an arm64 Dart SDK +
# engine), which renamed DialogTheme/TabBarTheme -> *Data and needs newer extended_text/
# google_fonts. Every other platform is still on Flutter 3.24.5, where the old names/versions
# are required, so these changes are kept OUT of the committed sources and applied here instead.
#
# Used by BOTH the Windows arm64 build (flutter-build.yml) and its dedicated bridge artifact
# (bridge.yml) so they share an identical 3.44 source state -- the generated *.freezed.dart must
# compile against the same Flutter/freezed version the arm64 build resolves.
#
# Remove this script (and commit the changes) once upstream bumps Flutter across the board.
#
# Run from the repository root. sed is used (not a git-apply patch) because the checked-out
# sources are CRLF on the windows-11-arm runner; the substitutions below are anchor-free and
# therefore CRLF-safe.
set -euo pipefail
# ThemeData API renames (Flutter 3.27+):
sed -i 's/dialogTheme: DialogTheme(/dialogTheme: DialogThemeData(/g' flutter/lib/common.dart
sed -i 's/tabBarTheme: const TabBarTheme(/tabBarTheme: const TabBarThemeData(/g' flutter/lib/common.dart
sed -i '/static ThemeData lightTheme = ThemeData(/,/static ThemeData darkTheme = ThemeData(/s/dialogTheme: DialogThemeData(/dialogTheme: DialogThemeData(\
backgroundColor: Colors.white,/' flutter/lib/common.dart
sed -i '/static ThemeData darkTheme = ThemeData(/,/scrollbarTheme: scrollbarThemeDark,/s/dialogTheme: DialogThemeData(/dialogTheme: DialogThemeData(\
backgroundColor: Color(0xFF18191E),/' flutter/lib/common.dart
# Dependency bumps required by the newer Dart/Flutter:
sed -i 's/extended_text: 14.0.0/extended_text: 15.0.2/' flutter/pubspec.yaml
sed -i 's/google_fonts: \^6.2.1/google_fonts: ^8.1.0/' flutter/pubspec.yaml
# Fail loudly if any expected string drifted, so we never silently build unpatched:
grep -qF 'dialogTheme: DialogThemeData(' flutter/lib/common.dart
grep -qF 'tabBarTheme: const TabBarThemeData(' flutter/lib/common.dart
grep -qF 'backgroundColor: Colors.white,' flutter/lib/common.dart
grep -qF 'backgroundColor: Color(0xFF18191E),' flutter/lib/common.dart
grep -qF 'extended_text: 15.0.2' flutter/pubspec.yaml
grep -qF 'google_fonts: ^8.1.0' flutter/pubspec.yaml
git --no-pager diff -- flutter/lib/common.dart flutter/pubspec.yaml
+23 -5
View File
@@ -7,7 +7,6 @@ on:
env:
CARGO_EXPAND_VERSION: "1.0.95"
FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -18,10 +17,21 @@ jobs:
fail-fast: false
matrix:
job:
# Default bridge for every platform still on Flutter 3.24.5 (generated with 3.22.3).
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
extra-build-args: "",
flutter-version: "3.22.3",
artifact-name: "bridge-artifact",
}
# Dedicated bridge for the Windows arm64 build (Flutter 3.44); runs in parallel.
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
extra-build-args: "",
flutter-version: "3.44.0",
artifact-name: "bridge-artifact-flutter-3.44",
}
steps:
- name: Checkout source code
@@ -64,13 +74,13 @@ jobs:
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
with:
path: /tmp/flutter_rust_bridge
key: vcpkg-${{ matrix.job.arch }}
key: bridge-${{ matrix.job.flutter-version }}
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
flutter-version: ${{ matrix.job.flutter-version }}
cache: true
- name: Install flutter rust bridge deps
@@ -78,7 +88,15 @@ jobs:
run: |
cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
if [[ "${{ matrix.job.flutter-version }}" == "3.22.3" ]]; then
# Default Flutter 3.22.3: extended_text 14 needs a newer Dart, so downgrade for resolution.
sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' flutter/pubspec.yaml
else
# Flutter 3.44 bridge for Windows arm64: match that build's source/pubspec state so the
# generated *.freezed.dart compiles against the same Flutter/freezed it resolves.
bash .github/patches/apply_flutter_3.44_source_patches.sh
fi
pushd flutter && flutter pub get && popd
- name: Run flutter rust bridge
run: |
@@ -88,7 +106,7 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: bridge-artifact
name: ${{ matrix.job.artifact-name }}
path: |
./src/bridge_generated.rs
./src/bridge_generated.io.rs
+1
View File
@@ -81,6 +81,7 @@ jobs:
# - { target: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
# - { target: aarch64-pc-windows-msvc , os: windows-11-arm }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:
+111 -17
View File
@@ -27,6 +27,11 @@ env:
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5"
# Windows arm64 only: the first stable Flutter to ship a native arm64 Windows Dart SDK +
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
FLUTTER_WINDOWS_ARM_VERSION: "3.44.0"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
@@ -39,7 +44,7 @@ env:
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.7"
VERSION: "1.4.8"
NDK_VERSION: "r28c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -53,14 +58,24 @@ jobs:
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
with:
upload-artifact: ${{ inputs.upload-artifact }}
target: windows-2022
configuration: Release
platform: x64
target_version: Windows10
strategy:
fail-fast: false
matrix:
job:
- {
target: windows-2022,
platform: x64,
}
- {
target: windows-11-arm,
platform: ARM64,
}
with:
upload-artifact: ${{ inputs.upload-artifact }}
target: ${{ matrix.job.target }}
configuration: Release
platform: ${{ matrix.job.platform }}
target_version: Windows10
build-for-windows-flutter:
name: ${{ matrix.job.target }}
@@ -76,9 +91,20 @@ jobs:
target: x86_64-pc-windows-msvc,
os: windows-2022,
arch: x86_64,
flutter-arch: x64,
vcpkg-triplet: x64-windows-static,
build-args: "--vram",
}
- {
target: aarch64-pc-windows-msvc,
os: windows-11-arm,
arch: aarch64,
flutter-arch: arm64,
vcpkg-triplet: arm64-windows-static,
# vram is x86/x64-only (NVENC needs CUDA, Intel MediaSDK needs __rdtsc);
# no NV/Intel/AMD hardware exists on Windows-on-ARM, so vram stays disabled here.
build-args: "",
}
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
@@ -95,36 +121,91 @@ jobs:
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
# arm64 is on Flutter 3.44, so it needs the bridge generated with the same Flutter
# (its *.freezed.dart must match the freezed the arm64 build resolves). x64 and every
# other platform keep the default 3.22.3-generated bridge.
name: ${{ matrix.job.arch == 'aarch64' && 'bridge-artifact-flutter-3.44' || 'bridge-artifact' }}
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@1a3da29f56261a1e1f937ec88f0856a9b8321d7e # v1
uses: KyleMayes/install-llvm-action@ebc0426251bc40c7cd31162802432c68818ab8f0 # v2.0.9
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
id: flutter
# arm64 builds with FLUTTER_WINDOWS_ARM_VERSION (>=3.44); x64 stays on FLUTTER_VERSION.
# subosito only ships an x64 Windows SDK (Flutter's release manifest lists x64 only),
# so it installs x64 on both arches. The arm64 runner re-bootstraps Dart to arm64 in
# the next step.
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
flutter-version: ${{ matrix.job.arch == 'aarch64' && env.FLUTTER_WINDOWS_ARM_VERSION || env.FLUTTER_VERSION }}
architecture: x64
- name: Force arm64 Dart SDK + engine
# The x64 SDK subosito installs bundles an x64 Dart with a matching engine-dart-sdk.stamp,
# so update_dart_sdk.ps1 short-circuits (stamp matches -> return) and keeps x64 Dart;
# `flutter build windows` then targets the Dart VM's arch = x64, even on this arm64 host.
# On this native-arm64 runner (PROCESSOR_ARCHITECTURE=ARM64), deleting the stamp and
# re-running update_dart_sdk.ps1 pulls the arm64 Dart (available since Flutter 3.44.0).
# https://github.com/flutter/flutter/issues/186730#issuecomment-4573214964
if: ${{ matrix.job.arch == 'aarch64' }}
run: |
$flutterRoot = "${{ steps.flutter.outputs['CACHE-PATH'] }}"
Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE"
Write-Host "Flutter root: $flutterRoot"
Remove-Item -Force "$flutterRoot\bin\cache\engine-dart-sdk.stamp" -ErrorAction SilentlyContinue
& "$flutterRoot\bin\internal\update_dart_sdk.ps1"
# Confirm the Dart we ended up with is arm64 ("on windows_arm64"); fail loudly if not.
$dartVer = & "$flutterRoot\bin\dart.bat" --version 2>&1 | Out-String
Write-Host $dartVer
if ($dartVer -notmatch "windows_arm64") {
Write-Error "Expected an arm64 Dart SDK but got: $dartVer"
exit 1
}
& "$flutterRoot\bin\flutter.bat" precache --windows
# Fail fast if precache pulled the wrong-arch Windows engine: an arm64 Dart should
# fetch windows-arm64 engine artifacts. Bailing here saves the ~25min Rust build.
$engineDir = "$flutterRoot\bin\cache\artifacts\engine"
Write-Host "Engine artifacts present:"
Get-ChildItem $engineDir -Directory | Select-Object -ExpandProperty Name | Write-Host
if (-not (Test-Path "$engineDir\windows-arm64-release")) {
Write-Error "Expected windows-arm64-release engine artifacts but they are missing (wrong-arch SDK)."
exit 1
}
# https://github.com/flutter/flutter/issues/155685
# x64 only: arm64 uses the stock native arm64 Windows engine, and the rustdesk/engine
# windows-x64-release.zip is built for the 3.24-era x64 engine (matches FLUTTER_VERSION).
- name: Replace engine with rustdesk custom flutter engine
if: ${{ matrix.job.arch == 'x86_64' }}
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Patch flutter
# x64 stays on Flutter 3.24.5, which needs the dropdown filter patch.
# arm64 is on Flutter 3.44 (patched separately below) and does not use this patch.
if: ${{ matrix.job.arch == 'x86_64' }}
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Patch RustDesk sources for Flutter 3.44 (arm64)
# arm64 is the only target on Flutter 3.44; apply its source/pubspec deltas on the fly
# (shared with the 3.44 bridge job) so the committed sources stay on Flutter 3.24.5.
# `flutter build` then runs `pub get`, regenerating pubspec.lock for the bumped deps.
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: bash .github/patches/apply_flutter_3.44_source_patches.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
@@ -163,11 +244,19 @@ jobs:
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Set SODIUM_LIB_DIR (arm64)
# libsodium-sys ships no arm64 Windows prebuilt lib; point it at the vcpkg-built one
# (only for arm64 — leaving it unset lets x64 use the crate's bundled lib).
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: echo "SODIUM_LIB_DIR=$VCPKG_ROOT/installed/${{ matrix.job.vcpkg-triplet }}/lib" >> "$GITHUB_ENV"
- name: Build rustdesk
run: |
# Windows: build RustDesk
python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
# --hwcodec is shared by all Windows targets; per-target extras (e.g. --vram) come from the matrix
python3 .\build.py --portable --flutter --skip-portable-pack --hwcodec ${{ matrix.job.build-args }}
mv ./flutter/build/windows/${{ matrix.job.flutter-arch }}/runner/Release ./rustdesk
# Download usbmmidd_v2.zip and extract it to ./rustdesk
Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip
@@ -223,7 +312,7 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
name: ${{ matrix.job.arch == 'aarch64' && 'topmostwindow-artifacts-ARM64' || 'topmostwindow-artifacts-x64' }}
path: "./rustdesk"
- name: Upload unsigned
@@ -256,13 +345,18 @@ jobs:
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
- name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
# both x64 and ARM64 platform configs; WcaUtil/DUtil ship arm64 libs. msbuild platform
# is x64 / ARM64; the produced Package.msi is globbed since its bin/<platform>/ dir varies.
if: env.UPLOAD_ARTIFACT == 'true'
run: |
pushd ./res/msi
python preprocess.py --arp -d ../../rustdesk
nuget restore msi.sln
msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10
mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
msbuild msi.sln -p:Configuration=Release -p:Platform=$msiPlatform /p:TargetVersion=Windows10
$msi = Get-ChildItem ./Package/bin/*/Release/en-us/Package.msi | Select-Object -First 1
mv $msi.FullName ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
sha256sum ../../SignOutput/rustdesk-*.msi
- name: Sign rustdesk self-extracted file
+1 -1
View File
@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.7"
VERSION: "1.4.8"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -45,16 +45,15 @@ jobs:
run: |
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
- name: Build the project
run: |
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
cd RustDeskTempTopMostWindow && git checkout ecd8d6a139eee76845ea66423fb739af450fda90
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
- name: Archive build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
name: topmostwindow-artifacts-${{ inputs.platform }}
path: |
./${{ env.build_output_dir }}/WindowInjection.dll
Generated
+19 -20
View File
@@ -292,7 +292,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arboard"
version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914"
source = "git+https://github.com/rustdesk-org/arboard#c7d5781f563176df9efd8df6287e823fb1b9bed5"
dependencies = [
"clipboard-win",
"core-graphics 0.23.2",
@@ -1324,7 +1324,7 @@ dependencies = [
[[package]]
name = "clipboard-master"
version = "4.0.0-beta.6"
source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809"
source = "git+https://github.com/rustdesk-org/clipboard-master#7762d74e38db37cfeb6ded88c964b9cdbddfb6db"
dependencies = [
"objc",
"objc-foundation",
@@ -2329,7 +2329,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.4",
"libloading 0.7.4",
]
[[package]]
@@ -2694,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3952,7 +3952,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.7.1"
source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738"
source = "git+https://github.com/rustdesk-org/hwcodec#778df1f99597722473b29443bac22ae6c23946fe"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -4494,7 +4494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if 1.0.0",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -4695,7 +4695,7 @@ dependencies = [
[[package]]
name = "magnum-opus"
version = "0.4.0"
source = "git+https://github.com/rustdesk-org/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256"
source = "git+https://github.com/rustdesk-org/magnum-opus#588c6e1f9ed50c3a01fa64f3bd3e7cdb0378a114"
dependencies = [
"bindgen 0.59.2",
"pkg-config",
@@ -6673,7 +6673,7 @@ dependencies = [
"once_cell",
"socket2 0.5.10",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -6920,7 +6920,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
source = "git+https://github.com/rustdesk-org/rdev#871bf1c856d6a30af2f56ab8848396a025140855"
dependencies = [
"cocoa 0.24.1",
"core-foundation 0.9.4",
@@ -7270,7 +7270,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.4.7"
version = "1.4.8"
dependencies = [
"android-wakelock",
"android_logger",
@@ -7385,7 +7385,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.8"
dependencies = [
"brotli",
"dirs 5.0.1",
@@ -7457,7 +7457,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7514,7 +7514,7 @@ dependencies = [
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -9733,9 +9733,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.3"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
dependencies = [
"bitflags 2.9.1",
"wayland-backend",
@@ -10838,16 +10838,15 @@ dependencies = [
[[package]]
name = "wl-clipboard-rs"
version = "0.9.0"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 0.38.34",
"tempfile",
"thiserror 1.0.61",
"rustix 1.1.2",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.4.7"
version = "1.4.8"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
+1 -1
View File
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.8
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
+1 -1
View File
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.8
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
+9 -2
View File
@@ -17,7 +17,8 @@ osx = platform.platform().startswith(
hbb_name = 'rustdesk' + ('.exe' if windows else '')
exe_path = 'target/release/' + hbb_name
if windows:
flutter_build_dir = 'build/windows/x64/runner/Release/'
win_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x64'
flutter_build_dir = f'build/windows/{win_arch}/runner/Release/'
elif osx:
flutter_build_dir = 'build/macos/Build/Products/Release/'
else:
@@ -410,7 +411,12 @@ def build_flutter_dmg(version, features):
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
system2('flutter build macos --release')
# cargo builds a single-arch dylib for the host; restrict Xcode to the same arch
# so the universal-by-default ARCHS_STANDARD doesn't try to link a missing slice.
# FLUTTER_XCODE_* env vars are forwarded to xcodebuild as build settings.
mac_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x86_64'
system2(
f'FLUTTER_XCODE_ARCHS={mac_arch} FLUTTER_XCODE_ONLY_ACTIVE_ARCH=YES flutter build macos --release')
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
system2(
@@ -506,6 +512,7 @@ def main():
'target\\release\\rustdesk.exe')
else:
print('Not signed')
os.makedirs(res_dir, exist_ok=True)
system2(
f'cp -rf target/release/RustDesk.exe {res_dir}')
os.chdir('libs/portable')
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199"><path fill="#0089d6" d="M118.432 187.698c32.89-5.81 60.055-10.618 60.367-10.684l.568-.12-31.052-36.935c-17.078-20.314-31.051-37.014-31.051-37.11 0-.182 32.063-88.477 32.243-88.792.06-.105 21.88 37.567 52.893 91.32 29.035 50.323 52.973 91.815 53.195 92.203l.405.707-98.684-.012-98.684-.013 59.8-10.564zM0 176.435c0-.052 14.631-25.451 32.514-56.442l32.514-56.347 37.891-31.799C123.76 14.358 140.867.027 140.935.001c.069-.026-.205.664-.609 1.534s-18.919 40.582-41.145 88.25l-40.41 86.67-29.386.037c-16.162.02-29.385-.005-29.385-.057z"/></svg>

Before

Width:  |  Height:  |  Size: 604 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="#000000" fill-rule="evenodd">
<rect x="4" y="6" width="24" height="16" rx="3"/>
<rect x="14.5" y="22" width="3" height="2"/>
<rect x="9.5" y="24" width="13" height="2.5" rx="1.25"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 303 B

+69 -8
View File
@@ -598,6 +598,22 @@ class MyTheme {
}
}
/// Applies [fallbacks] as fontFamilyFallback to every text style in both
/// themes. Called once at startup on ARM64 Linux after a CJK font has been
/// loaded via FontLoader (see flutter/flutter#139293).
static void applyFontFallback(List<String> fallbacks) {
lightTheme = lightTheme.copyWith(
textTheme: lightTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
lightTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
darkTheme = darkTheme.copyWith(
textTheme: darkTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
darkTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {
@@ -3713,14 +3729,54 @@ Widget loadPowered(BuildContext context) {
).marginOnly(top: 6);
}
// max 300 x 60
Widget loadLogo() {
return FutureBuilder<ByteData>(
future: rootBundle.load('assets/logo.png'),
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
if (snapshot.hasData) {
const _kDefaultLogoAsset = 'assets/logo.png';
const _kLightLogoAsset = 'assets/logo_light.png';
const _kDarkLogoAsset = 'assets/logo_dark.png';
List<String> _logoAssetCandidatesForBrightness(Brightness brightness) {
return brightness == Brightness.dark
? [_kDarkLogoAsset, _kDefaultLogoAsset]
: [_kLightLogoAsset, _kDefaultLogoAsset];
}
Future<String?> _resolveLogoAsset(Brightness brightness) async {
for (final asset in _logoAssetCandidatesForBrightness(brightness)) {
try {
await rootBundle.load(asset);
return asset;
} on FlutterError {
continue;
}
}
return null;
}
class _Logo extends StatefulWidget {
const _Logo();
@override
State<_Logo> createState() => _LogoState();
}
class _LogoState extends State<_Logo> {
final Map<Brightness, Future<String?>> _logoFutures = {};
Future<String?> _logoFutureFor(Brightness brightness) {
return _logoFutures.putIfAbsent(
brightness,
() => _resolveLogoAsset(brightness),
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
future: _logoFutureFor(Theme.of(context).brightness),
builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
final asset = snapshot.data;
if (asset != null) {
final image = Image.asset(
'assets/logo.png',
asset,
fit: BoxFit.contain,
errorBuilder: (ctx, error, stackTrace) {
return Container();
@@ -3732,9 +3788,14 @@ Widget loadLogo() {
).marginOnly(left: 12, right: 12, top: 12);
}
return const Offstage();
});
},
);
}
}
// max 300 x 60
Widget loadLogo() => const _Logo();
Widget loadIcon(double size) {
return Image.asset('assets/icon.png',
width: size,
+216 -43
View File
@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart';
@@ -5,27 +8,136 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
@visibleForTesting
List<Peer> mergeAutocompletePeers({
Iterable<Peer> addressBookPeers = const [],
Iterable<Peer> groupPeers = const [],
Iterable<Peer> lanPeers = const [],
Iterable<Peer> recentPeers = const [],
Iterable<String> restRecentPeerIds = const [],
}) {
final combinedPeers = <String, Peer>{};
void addPeer(Peer peer) {
if (peer.id.isEmpty) {
return;
}
final existingPeer = combinedPeers[peer.id];
if (existingPeer == null) {
combinedPeers[peer.id] = Peer.copy(peer);
} else if (peer.online) {
existingPeer.online = true;
}
}
for (final peer in addressBookPeers) {
addPeer(peer);
}
for (final peer in groupPeers) {
addPeer(peer);
}
for (final peer in lanPeers) {
addPeer(peer);
}
for (final peer in recentPeers) {
addPeer(peer);
}
for (final id in restRecentPeerIds) {
if (id.isNotEmpty && !combinedPeers.containsKey(id)) {
combinedPeers[id] = Peer.fromJson({'id': id});
}
}
return combinedPeers.values.toList(growable: false);
}
@visibleForTesting
bool updateAutocompletePeerOnlineStates(
List<Peer> peers, {
required Set<String> onlines,
required Set<String> offlines,
}) {
var changed = false;
for (final peer in peers) {
if (onlines.contains(peer.id)) {
if (!peer.online) {
peer.online = true;
changed = true;
}
} else if (offlines.contains(peer.id)) {
if (peer.online) {
peer.online = false;
changed = true;
}
}
}
return changed;
}
@visibleForTesting
List<String> autocompleteOnlineQueryIds(
Iterable<Peer> options, {
required int limit,
}) {
final ids = <String>[];
final seenIds = <String>{};
for (final peer in options) {
if (peer.id.isEmpty || seenIds.contains(peer.id)) {
continue;
}
seenIds.add(peer.id);
ids.add(peer.id);
if (ids.length >= limit) {
break;
}
}
return ids;
}
class AllPeersLoader {
List<Peer> peers = [];
bool _isPeersLoading = false;
bool _isPeersLoaded = false;
Set<String> _lastQueryOnlineIds = {};
DateTime _lastQueryOnlineTime = DateTime.fromMillisecondsSinceEpoch(0);
Timer? _queryOnlineTimer;
List<Peer> _lastQueryOnlineOptions = const [];
Set<String> _lastOnlineIds = {};
Set<String> _lastOfflineIds = {};
final Future<void> Function(List<String> ids) _queryOnlines;
final Duration _queryOnlineDebounce;
void Function(VoidCallback)? _setState;
bool _isCleared = false;
final String _listenerKey = 'AllPeersLoader';
late void Function(VoidCallback) setState;
static const String _cbQueryOnlines = 'callback_query_onlines';
static const Duration _queryOnlineInterval = Duration(seconds: 5);
static const Duration _defaultQueryOnlineDebounce =
Duration(milliseconds: 300);
static const int _maxQueryOnlineOptions = 20;
bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
bool get isPeersLoaded => _isPeersLoaded;
AllPeersLoader();
AllPeersLoader({
@visibleForTesting Future<void> Function(List<String> ids)? queryOnlines,
@visibleForTesting Duration? queryOnlineDebounce,
}) : _queryOnlines = queryOnlines ?? ((ids) => bind.queryOnlines(ids: ids)),
_queryOnlineDebounce =
queryOnlineDebounce ?? _defaultQueryOnlineDebounce;
void init(void Function(VoidCallback) setState) {
this.setState = setState;
_setState = setState;
_isCleared = false;
gFFI.recentPeersModel.addListener(_mergeAllPeers);
gFFI.lanPeersModel.addListener(_mergeAllPeers);
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
platformFFI.registerEventHandler(_cbQueryOnlines, _listenerKey,
(evt) async {
_updateOnlineState(evt);
});
}
void clear() {
@@ -33,6 +145,11 @@ class AllPeersLoader {
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
gFFI.abModel.removePeerUpdateListener(_listenerKey);
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
platformFFI.unregisterEventHandler(_cbQueryOnlines, _listenerKey);
_queryOnlineTimer?.cancel();
_lastQueryOnlineOptions = const [];
_setState = null;
_isCleared = true;
}
Future<void> getAllPeers() async {
@@ -59,50 +176,106 @@ class AllPeersLoader {
}
void _mergeAllPeers() {
Map<String, dynamic> combinedPeers = {};
for (var p in gFFI.abModel.allPeers()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
if (_isCleared) {
return;
}
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
List<Peer> parsedPeers = [];
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
}
Set<String> peerIds = combinedPeers.keys.toSet();
for (final peer in gFFI.lanPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (final peer in gFFI.recentPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (final id in gFFI.recentPeersModel.restPeerIds) {
if (!peerIds.contains(id)) {
parsedPeers.add(Peer.fromJson({'id': id}));
peerIds.add(id);
}
}
peers = parsedPeers;
setState(() {
peers = mergeAutocompletePeers(
addressBookPeers: gFFI.abModel.allPeers(),
groupPeers: gFFI.groupModel.peers,
lanPeers: gFFI.lanPeersModel.peers,
recentPeers: gFFI.recentPeersModel.peers,
restRecentPeerIds: gFFI.recentPeersModel.restPeerIds,
);
_applyLastOnlineState(peers);
_scheduleSetState(() {
_isPeersLoading = false;
_isPeersLoaded = true;
});
}
void _updateOnlineState(Map<String, dynamic> evt) {
if (_isCleared) {
return;
}
_lastOnlineIds = _splitPeerIds(evt['onlines']);
_lastOfflineIds = _splitPeerIds(evt['offlines']);
final peersChanged = _applyLastOnlineState(peers);
final optionsChanged = _applyLastOnlineState(_lastQueryOnlineOptions);
if (peersChanged || optionsChanged) {
_scheduleSetState(() {});
}
}
void _scheduleSetState(VoidCallback callback) {
if (_isCleared) {
return;
}
final setState = _setState;
if (setState == null) {
callback();
} else {
setState(callback);
}
}
bool _applyLastOnlineState(List<Peer> peers) {
return updateAutocompletePeerOnlineStates(
peers,
onlines: _lastOnlineIds,
offlines: _lastOfflineIds,
);
}
Set<String> _splitPeerIds(dynamic ids) {
if (ids is! String || ids.isEmpty) {
return {};
}
return ids.split(',').where((id) => id.isNotEmpty).toSet();
}
void queryOnlines(Iterable<Peer> options) {
if (_isCleared) {
return;
}
_lastQueryOnlineOptions = options.toList(growable: false);
final ids = autocompleteOnlineQueryIds(
_lastQueryOnlineOptions,
limit: _maxQueryOnlineOptions,
).toSet();
_queryOnlineTimer?.cancel();
_queryOnlineTimer = null;
if (ids.isEmpty) {
return;
}
final now = DateTime.now();
if (setEquals(ids, _lastQueryOnlineIds) &&
now.difference(_lastQueryOnlineTime) < _queryOnlineInterval) {
return;
}
_queryOnlineTimer = Timer(_queryOnlineDebounce, () async {
try {
await _queryOnlines(ids.toList(growable: false));
if (_isCleared) {
return;
}
_lastQueryOnlineIds = ids;
_lastQueryOnlineTime = DateTime.now();
} catch (e) {
debugPrint('query autocomplete online state failed: $e');
}
});
}
@visibleForTesting
void updateOnlineStateForTesting(Map<String, dynamic> evt) {
_updateOnlineState(evt);
}
@visibleForTesting
bool applyLastOnlineStateForTesting(List<Peer> peers) {
return _applyLastOnlineState(peers);
}
}
class AutocompletePeerTile extends StatefulWidget {
+33 -8
View File
@@ -24,6 +24,35 @@ const kOpSvgList = [
'microsoft'
];
class _OidcProviderBranding {
final String label;
final String iconKey;
const _OidcProviderBranding({
required this.label,
required this.iconKey,
});
}
_OidcProviderBranding _oidcProviderBranding(String op) {
switch (op.toLowerCase()) {
case 'azure':
return _OidcProviderBranding(
label: 'Microsoft',
iconKey: 'microsoft',
);
default:
return _OidcProviderBranding(
label: {
'github': 'GitHub',
'gitlab': 'GitLab',
}[op.toLowerCase()] ??
toCapitalized(op),
iconKey: op.toLowerCase(),
);
}
}
class _IconOP extends StatelessWidget {
final String op;
final String? icon;
@@ -74,11 +103,8 @@ class ButtonOP extends StatelessWidget {
@override
Widget build(BuildContext context) {
final opLabel = {
'github': 'GitHub',
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
final branding = _oidcProviderBranding(op);
final buttonLabel = translate("Continue with {${branding.label}}");
return Row(children: [
Container(
height: height,
@@ -95,7 +121,7 @@ class ButtonOP extends StatelessWidget {
SizedBox(
width: 30,
child: _IconOP(
op: op,
op: branding.iconKey,
icon: icon,
margin: EdgeInsets.only(right: 5),
),
@@ -103,8 +129,7 @@ class ButtonOP extends StatelessWidget {
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text(translate("Continue with {$opLabel}"))),
child: Center(child: Text(buttonLabel)),
),
),
],
+6 -18
View File
@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => TapGestureRecognizer(), (instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => DoubleTapGestureRecognizer(), (instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap;
}),
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => LongPressGestureRecognizer(), (instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
),
() => HoldTapMoveGestureRecognizer(),
(instance) => instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
..onHoldDragEnd = onHoldDragEnd),
DoubleFinerTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => DoubleFinerTapGestureRecognizer(), (instance) {
instance
..onDoubleFinerTap = onDoubleFinerTap
..onDoubleFinerTapDown = onDoubleFinerTapDown;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => CustomTouchGestureRecognizer(), (instance) {
instance.onOneFingerPanStart =
(DragStartDetails d) => onOneFingerPanStart(context, d);
instance
+40 -18
View File
@@ -72,10 +72,24 @@ Widget waylandKeyboardScopeChip(BuildContext context, String text) {
);
}
// macOS privacy mode blacks out all online displays, so switching the remote
// display does not weaken the local privacy protection.
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
return pi.platform == kPeerPlatformMacOS;
bool _isWindowsMode1PrivacyImpl(String privacyModeImpl) {
return privacyModeImpl == kPrivacyModeImplMag ||
privacyModeImpl == kPrivacyModeImplExcludeFromCapture;
}
// macOS privacy mode blacks out all online displays. Windows Mode 1 also
// covers every local monitor with privacy overlay windows, so remote display
// switching does not weaken local privacy protection.
//
// Keep this separate from the capture backend capability. The legacy Windows
// magnifier capturer is not reliable for multi-monitor capture; WebRTC's
// screen_capturer_win_magnifier also disables it when SM_CMONITORS != 1:
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi, String privacyModeImpl) {
return pi.platform == kPeerPlatformMacOS ||
(pi.platform == kPeerPlatformWindows &&
_isWindowsMode1PrivacyImpl(privacyModeImpl) &&
versionCmp(pi.version, '1.4.8') >= 0);
}
class TTextMenu {
@@ -964,7 +978,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
(privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -1048,7 +1063,20 @@ List<TToggleMenu> toolbarPrivacyMode(
return []; // No permission and not active, hide options.
}
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
bool checkDisplayAllowedForPrivacyMode(String targetImplKey, bool turnOn) {
if (!turnOn ||
allowDisplaySwitchInPrivacyMode(pi, targetImplKey) ||
(ffiModel.pi.currentDisplay == 0 &&
!bind.sessionIsMultiUiSession(sessionId: sessionId))) {
return true;
}
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
'Please switch to Display 1 first', '', ffi.dialogManager);
return false;
}
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc,
String targetImplKey) {
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
return TToggleMenu(
@@ -1056,16 +1084,7 @@ List<TToggleMenu> toolbarPrivacyMode(
onChanged: enabled
? (value) {
if (value == null) return;
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
'custom-nook-nocancel-hasclose',
'info',
'Please switch to Display 1 first',
'',
ffi.dialogManager);
if (!checkDisplayAllowedForPrivacyMode(targetImplKey, value)) {
return;
}
final option = 'privacy-mode';
@@ -1083,7 +1102,7 @@ List<TToggleMenu> toolbarPrivacyMode(
getDefaultMenu((sid, opt) async {
bind.sessionToggleOption(sessionId: sid, value: opt);
togglePrivacyModeTime = DateTime.now();
})
}, kPrivacyModeImplMag)
];
}
if (privacyModeImpls.isEmpty) {
@@ -1097,7 +1116,7 @@ List<TToggleMenu> toolbarPrivacyMode(
bind.sessionTogglePrivacyMode(
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
togglePrivacyModeTime = DateTime.now();
})
}, implKey)
];
} else {
final visibleImpls = hasPrivacyModePermission
@@ -1118,6 +1137,9 @@ List<TToggleMenu> toolbarPrivacyMode(
? (value) {
if (value == null) return;
if (value && !hasPrivacyModePermission) return;
if (!checkDisplayAllowedForPrivacyMode(implKey, value)) {
return;
}
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
+6
View File
@@ -29,6 +29,10 @@ const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
const String kPrivacyModeImplMag = 'privacy_mode_impl_mag';
const String kPrivacyModeImplExcludeFromCapture =
'privacy_mode_impl_exclude_from_capture';
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
@@ -170,6 +174,8 @@ const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
const String kOptionAllowMonitorSwitchMainToolbar = "allow-monitor-switch-main-toolbar";
const String kOptionAllowMonitorSwitchMinToolbar = "allow-monitor-switch-min-toolbar";
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
// network options
@@ -398,6 +398,7 @@ class _ConnectionPageState extends State<ConnectionPage>
.contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind))
.toList();
_allPeersLoader.queryOnlines(_autocompleteOpts);
}
return _autocompleteOpts;
},
@@ -407,6 +407,7 @@ class _GeneralState extends State<_General> {
final RxBool serviceStop =
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs;
final GlobalKey _minToolbarOptionKey = GlobalKey();
@override
Widget build(BuildContext context) {
@@ -605,6 +606,47 @@ class _GeneralState extends State<_General> {
},
));
}
children.add(_OptionCheckBox(
context,
'Show monitor switch button on the main toolbar',
kOptionAllowMonitorSwitchMainToolbar,
isServer: false,
update: (enabled) async {
if (!enabled) {
await mainSetLocalBoolOption(
kOptionAllowMonitorSwitchMinToolbar, false);
}
if (mounted) setState(() {});
reloadAllWindows();
if (enabled) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = _minToolbarOptionKey.currentContext;
if (ctx != null) {
Scrollable.ensureVisible(
ctx,
alignment: 0.5,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
}
});
}
},
));
if (mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
children.add(KeyedSubtree(
key: _minToolbarOptionKey,
child: _OptionCheckBox(
context,
'Show on the minimized toolbar',
kOptionAllowMonitorSwitchMinToolbar,
isServer: false,
update: (_) {
reloadAllWindows();
},
).marginOnly(left: _kCheckBoxLeftMargin * 3),
));
}
return _Card(title: 'Other', children: children);
}
+2 -2
View File
@@ -65,7 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
final RxBool printer = true.obs;
final RxBool printer = false.obs;
final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs;
@@ -80,7 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
printer.value = installOptions['PRINTER'] != '0';
printer.value = installOptions['PRINTER'] == '1';
}
@override
+172 -2
View File
@@ -779,6 +779,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
borderRadius: borderRadius,
child: _DraggableShowHide(
id: widget.id,
ffi: widget.ffi,
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fraction: _fraction,
@@ -805,13 +806,25 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(Obx(() {
final privacyModeState = PrivacyModeState.find(widget.id);
if ((privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
pi.displaysCount.value > 1 &&
mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
return _MainMonitorSwitchButton(id: widget.id, ffi: widget.ffi);
} else {
return const Offstage();
}
}));
if (!isWebDesktop) {
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
}
toolbarItems.add(Obx(() {
if ((PrivacyModeState.find(widget.id).isEmpty ||
allowDisplaySwitchInPrivacyMode(pi)) &&
final privacyModeState = PrivacyModeState.find(widget.id);
if ((privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,
@@ -964,6 +977,88 @@ class _MobileActionMenu extends StatelessWidget {
}
}
class _MonitorCycle {
final String id;
final FFI ffi;
const _MonitorCycle(this.id, this.ffi);
PeerInfo get _pi => ffi.ffiModel.pi;
int get total => _pi.displays.length;
int get _current => CurrentDisplayState.find(id).value;
bool get _inRange => _current >= 0 && _current < total;
String get label => _inRange ? '${_current + 1}' : '*';
String get tooltip => '${translate('Switch display')} ($label/$total)';
void next() {
final t = total;
if (t < 2) return;
final from = _inRange ? _current : -1;
final target = (from + 1) % t;
final isChooseDisplayToOpenInNewWindow = _pi.isSupportMultiDisplay &&
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
'Y';
if (isChooseDisplayToOpenInNewWindow) {
openMonitorInNewTabOrWindow(target, ffi.id, _pi);
} else {
openMonitorInTheSameTab(target, ffi, _pi, updateCursorPos: false);
}
}
}
class _MainMonitorSwitchButton extends StatelessWidget {
final String id;
final FFI ffi;
const _MainMonitorSwitchButton({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final cycle = _MonitorCycle(id, ffi);
return Obx(() {
if (cycle.total < 2) return const Offstage();
final label = cycle.label;
return _IconMenuButton(
tooltip: cycle.tooltip,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
onPressed: cycle.next,
icon: SizedBox(
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
child: Stack(
alignment: const Alignment(0, -0.125),
children: [
SvgPicture.asset(
'assets/display_switcher.svg',
colorFilter:
const ColorFilter.mode(Colors.white, BlendMode.srcIn),
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
fontSize: 11,
height: 1,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
});
}
}
class _MonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
@@ -2970,6 +3065,7 @@ class RdoMenuButton<T> extends StatelessWidget {
class _DraggableShowHide extends StatefulWidget {
final String id;
final FFI ffi;
final SessionID sessionId;
final RxDouble fraction;
final Rx<_ToolbarEdge> edge;
@@ -2993,6 +3089,7 @@ class _DraggableShowHide extends StatefulWidget {
const _DraggableShowHide({
Key? key,
required this.id,
required this.ffi,
required this.sessionId,
required this.fraction,
required this.edge,
@@ -3249,6 +3346,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
Obx(() => collapse.isTrue
? _MinimizedMonitorSwitchButton(id: widget.id, ffi: widget.ffi)
: const Offstage()),
Obx(() => buttonWrapper(
() {
widget.setFullscreen(!isFullscreen.value);
@@ -3409,3 +3509,73 @@ class EdgeThicknessControl extends StatelessWidget {
return slider;
}
}
class _MinimizedMonitorSwitchButton extends StatelessWidget {
final String id;
final FFI ffi;
const _MinimizedMonitorSwitchButton({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const double iconSize = 20;
final cycle = _MonitorCycle(id, ffi);
return Obx(() {
final label = cycle.label;
if (!mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar) ||
!mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMinToolbar)) {
return const Offstage();
}
if (cycle.total < 2) return const Offstage();
final privacyModeState = PrivacyModeState.find(id);
if (privacyModeState.isNotEmpty &&
!allowDisplaySwitchInPrivacyMode(
ffi.ffiModel.pi, privacyModeState.value)) {
return const Offstage();
}
return Tooltip(
message: cycle.tooltip,
child: TextButton(
onPressed: cycle.next,
style: ButtonStyle(
minimumSize: MaterialStateProperty.all(const Size(0, 0)),
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered)) {
return _ToolbarTheme.blueColor.withOpacity(0.15);
}
return null;
}),
),
child: Stack(
alignment: const Alignment(0, -0.125),
children: [
SvgPicture.asset(
'assets/display_switcher.svg',
colorFilter:
ColorFilter.mode(_ToolbarTheme.blueColor, BlendMode.srcIn),
width: iconSize,
height: iconSize,
),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 9,
height: 1,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
});
}
}
+22
View File
@@ -29,6 +29,8 @@ import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'native/font_manager.dart'
if (dart.library.html) 'web/font_manager.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
@@ -37,10 +39,15 @@ import 'package:flutter_hbb/plugin/handlers.dart'
int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
bool _cjkFontLoaded = false;
Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized();
_cjkFontLoaded = await loadSystemCJKFonts();
if (_cjkFontLoaded) {
MyTheme.applyFontFallback([kLinuxCjkFontFamily]);
}
debugPrint("launch args: $args");
kBootArgs = List.from(args);
@@ -383,6 +390,7 @@ void _runApp(
builder: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
return child;
},
),
@@ -533,6 +541,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child);
@@ -586,6 +595,19 @@ _registerEventHandler() {
}
}
/// Merges the theme's fontFamilyFallback into [DefaultTextStyle] so that
/// bare [Text] widgets (and those with inherit:true styles) also pick up the
/// CJK fallback font loaded on ARM64 Linux.
Widget _mergeCjkFallback(BuildContext context, Widget? child) {
final result = child ?? Container();
final fallback = Theme.of(context).textTheme.bodyMedium?.fontFamilyFallback;
if (fallback == null || fallback.isEmpty) return result;
return DefaultTextStyle.merge(
style: TextStyle(fontFamilyFallback: fallback),
child: result,
);
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {
return RawKeyboardListener(
focusNode: FocusNode(),
@@ -207,6 +207,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
.contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind))
.toList();
_allPeersLoader.queryOnlines(_autocompleteOpts);
}
return _autocompleteOpts;
},
+11 -7
View File
@@ -517,10 +517,12 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
return Container(
color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
);
}),
),
@@ -1218,7 +1220,11 @@ void showOptions(
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
final privacyModeState = PrivacyModeState.find(id);
if (pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
(privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value))) {
final cur = pi.currentDisplay;
final children = <Widget>[];
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
@@ -1272,8 +1278,6 @@ void showOptions(
await toolbarDisplayToggle(context, id, gFFI);
List<TToggleMenu> privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
privacyModeState.isNotEmpty) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State<ViewCameraPage>
}
return Container(
color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
);
}),
),
+38 -11
View File
@@ -117,13 +117,13 @@ void showServerSettingsWithValue(
),
SizedBox(width: 8),
Expanded(
child: TextFormField(
child: serverSettingsTextFormField(
label: label,
controller: controller,
decoration: InputDecoration(
errorText: errorMsg.isEmpty ? null : errorMsg,
contentPadding:
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
),
errorMsg: errorMsg,
contentPadding:
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
showLabelText: false,
validator: validator,
autofocus: autofocus,
).workaroundFreezeLinuxMint(),
@@ -132,12 +132,10 @@ void showServerSettingsWithValue(
);
}
return TextFormField(
return serverSettingsTextFormField(
label: label,
controller: controller,
decoration: InputDecoration(
labelText: label,
errorText: errorMsg.isEmpty ? null : errorMsg,
),
errorMsg: errorMsg,
validator: validator,
).workaroundFreezeLinuxMint();
}
@@ -209,6 +207,35 @@ void showServerSettingsWithValue(
});
}
TextFormField serverSettingsTextFormField({
required String label,
required TextEditingController controller,
required String errorMsg,
String? Function(String?)? validator,
bool autofocus = false,
bool showLabelText = true,
EdgeInsetsGeometry? contentPadding,
}) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: showLabelText ? label : null,
errorText: errorMsg.isEmpty ? null : errorMsg,
contentPadding: contentPadding,
),
validator: validator,
autofocus: autofocus,
keyboardType: TextInputType.visiblePassword,
textCapitalization: TextCapitalization.none,
autocorrect: false,
enableSuggestions: false,
smartDashesType: SmartDashesType.disabled,
smartQuotesType: SmartQuotesType.disabled,
enableIMEPersonalizedLearning: false,
spellCheckConfiguration: const SpellCheckConfiguration.disabled(),
);
}
void setPrivacyModeDialog(
OverlayDialogManager dialogManager,
List<TToggleMenu> privacyModeList,
+35 -4
View File
@@ -1307,7 +1307,8 @@ class InputModel {
}
if (isPhysicalMouse.value) {
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
final canvasPosition = _pointerPositionForRemoteCanvas(e);
handleMouse(_getMouseEvent(e, _kMouseEventMove), canvasPosition,
edgeScroll: useEdgeScroll);
}
}
@@ -1548,7 +1549,8 @@ class InputModel {
_relativeMouse
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
} else {
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
final canvasPosition = _pointerPositionForRemoteCanvas(e);
handleMouse(_getMouseEvent(e, _kMouseEventDown), canvasPosition);
}
}
}
@@ -1570,7 +1572,8 @@ class InputModel {
_relativeMouse
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
} else {
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
final canvasPosition = _pointerPositionForRemoteCanvas(e);
handleMouse(_getMouseEvent(e, _kMouseEventUp), canvasPosition);
}
}
}
@@ -1592,12 +1595,40 @@ class InputModel {
}
if (isPhysicalMouse.value) {
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
final canvasPosition = _pointerPositionForRemoteCanvas(e);
handleMouse(_getMouseEvent(e, _kMouseEventMove), canvasPosition,
edgeScroll: useEdgeScroll);
}
}
}
/// Convert pointer coordinates into the visible remote canvas space.
///
/// On mobile, the remote page body is wrapped in `SafeArea`, but the pointer
/// listener that feeds these events sits outside that subtree. As a result,
/// `event.localPosition` still includes the top/left safe-area inset.
///
/// When the keyboard-visible path shows `KeyHelpTools`, the remote canvas is
/// also shifted downward by `CanvasModel.getAdjustY()`. The downstream mouse
/// mapping logic expects coordinates relative to the visible canvas area, so
/// we subtract both the mobile safe-area padding and the current canvas
/// adjustment before passing the position into mouse mapping.
///
/// Desktop and web desktop continue to use the global position directly
/// because their pointer mapping is window-based.
Offset _pointerPositionForRemoteCanvas(PointerEvent event) {
if (isDesktop || isWebDesktop) {
return event.position;
}
final mediaData = MediaQueryData.fromView(
WidgetsBinding.instance.platformDispatcher.views.first);
final adjustY = parent.target?.canvasModel.getAdjustY() ?? 0.0;
return Offset(
event.localPosition.dx - mediaData.padding.left,
event.localPosition.dy - mediaData.padding.top - adjustY,
);
}
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
List<RemoteWindowCoords> remoteWindowCoords) async {
final coords =
+35 -2
View File
@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
final _constSessionId = Uuid().v4obj();
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
const _restartReconnectSilentDelaySecs = 5;
class CachedPeerData {
Map<String, dynamic> updatePrivacyMode = {};
@@ -119,6 +121,7 @@ class FfiModel with ChangeNotifier {
bool _touchMode = false;
late VirtualMouseMode virtualMouseMode;
Timer? _timer;
Timer? _restartReconnectDelayTimer;
var _reconnects = 1;
DateTime? _offlineReconnectStartTime;
bool _viewOnly = false;
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
_inputBlocked = false;
_timer?.cancel();
_timer = null;
resetRestartReconnectState();
clearPermissions();
waitForImageTimer?.cancel();
timerScreenshot?.cancel();
@@ -341,6 +345,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'connection_ready') {
setConnectionType(peerId, evt['secure'] == 'true',
evt['direct'] == 'true', evt['stream_type'] ?? '');
resetRestartReconnectState();
} else if (name == 'switch_display') {
// switch display is kept for backward compatibility
handleSwitchDisplay(evt, sessionId, peerId);
@@ -922,8 +927,28 @@ class FfiModel with ChangeNotifier {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'restarting') {
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
hasCancel: false);
// Treat restart messages as reconnect control events. Rust still sends
// title/text for legacy UI and translation reuse; Flutter keeps the last
// frame briefly, then shows the Connecting overlay.
if (_restartReconnectDelayTimer == null) {
parent.target?.inputModel.setRelativeMouseMode(false);
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
clearPermissions();
// Retry once more after the silent window so restart reconnect attempts
// are spaced by the empirical short cadence instead of only updating UI.
_restartReconnectDelayTimer =
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
_restartReconnectDelayTimer = null;
if (parent.target?.closed == true) {
return;
}
reconnect(dialogManager, sessionId, false);
});
}
} else if (type == 'restarting-show') {
_restartReconnectDelayTimer?.cancel();
_restartReconnectDelayTimer = null;
reconnect(dialogManager, sessionId, false);
} else if (type == 'wait-remote-accept-nook') {
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
@@ -949,6 +974,11 @@ class FfiModel with ChangeNotifier {
}
}
void resetRestartReconnectState() {
_restartReconnectDelayTimer?.cancel();
_restartReconnectDelayTimer = null;
}
/// Auto-retry check for "Remote desktop is offline" error.
/// returns true to auto-retry, false otherwise.
bool shouldAutoRetryOnOffline(
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
if (displays.isNotEmpty) {
_reconnects = 1;
_offlineReconnectStartTime = null;
resetRestartReconnectState();
waitForFirstImage.value = true;
isRefreshing = false;
}
@@ -3666,6 +3697,7 @@ class FFI {
/// Mobile reuse FFI
void mobileReset() {
ffiModel.resetRestartReconnectState();
ffiModel.waitForFirstImage.value = true;
ffiModel.isRefreshing = false;
ffiModel.waitForImageDialogShow.value = true;
@@ -3879,6 +3911,7 @@ class FFI {
}
if (ffiModel.waitForFirstImage.value == true) {
ffiModel.waitForFirstImage.value = false;
ffiModel.resetRestartReconnectState();
dialogManager.dismissAll();
await canvasModel.updateViewStyle();
await canvasModel.updateScrollStyle();
+20 -17
View File
@@ -145,23 +145,26 @@ class Peer {
note == other.note;
}
Peer.copy(Peer other)
: this(
id: other.id,
hash: other.hash,
password: other.password,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
device_group_name: other.device_group_name,
note: other.note,
sameServer: other.sameServer);
factory Peer.copy(Peer other) {
final peer = Peer(
id: other.id,
hash: other.hash,
password: other.password,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
device_group_name: other.device_group_name,
note: other.note,
sameServer: other.sameServer);
peer.online = other.online;
return peer;
}
}
enum UpdateEvent { online, load }
+109
View File
@@ -0,0 +1,109 @@
import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Font family name registered with [FontLoader] when a system CJK font is
/// successfully loaded on ARM64 Linux.
const kLinuxCjkFontFamily = 'SystemCJK';
const _kFontSearchPaths = [
// Debian / Ubuntu (noto-fonts / fonts-noto-cjk)
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf',
// Fedora / RHEL / Rocky (google-noto-sans-cjk-fonts)
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc',
// Arch Linux (noto-fonts-cjk)
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf',
// Generic fallback paths
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto/NotoSansCJKsc-Regular.otf',
// WenQuanYi commonly pre-installed on CJK-locale systems
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc',
'/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc',
];
/// Loads a system CJK font on ARM64 Linux into Flutter's font registry via
/// [FontLoader], working around the missing fontconfig support in the
/// flutter-elinux engine (https://github.com/flutter/flutter/issues/139293).
///
/// Returns true if a CJK font was successfully loaded; false otherwise.
/// On all other platforms this is a no-op and returns false immediately.
Future<bool> loadSystemCJKFonts() async {
if (Abi.current() != Abi.linuxArm64) return false;
final path = await _findCjkFontPath();
if (path == null) {
debugPrint('ARM64 Linux: no CJK font found; CJK text may not render');
return false;
}
try {
final loader = FontLoader(kLinuxCjkFontFamily);
final bytes = await File(path).readAsBytes();
loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes)));
await loader.load();
debugPrint('ARM64 Linux: loaded CJK font from $path');
return true;
} catch (e) {
debugPrint('ARM64 Linux: failed to load CJK font: $e');
return false;
}
}
Future<String?> _findCjkFontPath() async {
// Query fc-list for each CJK script separately. Fonts present in all three
// sets (zh ja ko) are true pan-CJK fonts; prefer them so we don't
// accidentally pick a Chinese-only font that lacks Japanese kana or Korean
// hangul glyphs. fc-list is a fontconfig CLI tool available on most Linux
// systems independent of whether the Flutter engine was built with fontconfig.
final byLang = <String, Set<String>>{};
for (final lang in const ['zh', 'ja', 'ko']) {
final paths = <String>{};
try {
final r =
await Process.run('fc-list', [':lang=$lang', '--format=%{file}\n']);
if (r.exitCode == 0) {
for (final line in r.stdout.toString().split('\n')) {
final p = line.trim();
if (p.isNotEmpty && File(p).existsSync()) paths.add(p);
}
}
} catch (e) {
debugPrint('ARM64 Linux: fc-list failed for lang=$lang: $e');
}
byLang[lang] = paths;
}
final panCjk = byLang['zh']!
.intersection(byLang['ja']!)
.intersection(byLang['ko']!);
final anyCjk =
byLang.values.fold(<String>{}, (acc, s) => acc..addAll(s));
// Among candidates, prefer well-known pan-CJK font families.
String? pick(Iterable<String> pool) {
const preferred = ['notosanscjk', 'sourcehansans', 'sourcehanserif'];
for (final name in preferred) {
for (final p in pool) {
if (p.toLowerCase().contains(name)) return p;
}
}
return pool.isNotEmpty ? pool.first : null;
}
final found = pick(panCjk) ?? pick(anyCjk);
if (found != null) return found;
for (final p in _kFontSearchPaths) {
if (File(p).existsSync()) return p;
}
return null;
}
+8
View File
@@ -0,0 +1,8 @@
/// Web stub for `native/font_manager.dart`.
///
/// The native implementation depends on `dart:io` (Process/File/Platform) to
/// load a system CJK font on ARM64 Linux, which cannot compile for the web
/// target. The web build has no such fontconfig limitation, so this is a no-op.
const kLinuxCjkFontFamily = 'SystemCJK';
Future<bool> loadSystemCJKFonts() async => false;
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.4.7+65
version: 1.4.8+66
environment:
sdk: '^3.1.0'
@@ -0,0 +1,148 @@
import 'package:flutter_hbb/common/widgets/autocomplete.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_test/flutter_test.dart';
Peer _peer({
required String id,
String alias = '',
String username = '',
String hostname = '',
bool online = false,
}) {
final peer = Peer(
id: id,
username: username,
hostname: hostname,
alias: alias,
platform: '',
tags: [],
hash: '',
password: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
loginName: '',
device_group_name: '',
note: '',
);
peer.online = online;
return peer;
}
void main() {
test('merged autocomplete peers keep address book metadata and online state',
() {
final peers = mergeAutocompletePeers(
addressBookPeers: [
_peer(id: '123456789', alias: 'Office PC', username: 'ab-user'),
],
lanPeers: [
_peer(id: '123456789', username: 'lan-user', online: true),
],
);
expect(peers, hasLength(1));
expect(peers.single.id, '123456789');
expect(peers.single.alias, 'Office PC');
expect(peers.single.username, 'ab-user');
expect(peers.single.online, isTrue);
});
test('peer copies preserve online state', () {
final peer = _peer(id: '987654321', online: true);
expect(Peer.copy(peer).online, isTrue);
});
test('online callbacks update autocomplete-only peers', () {
final peers = mergeAutocompletePeers(restRecentPeerIds: ['112233445']);
final changed = updateAutocompletePeerOnlineStates(
peers,
onlines: {'112233445'},
offlines: {},
);
expect(changed, isTrue);
expect(peers.single.online, isTrue);
});
test('online query ids are deduplicated and limited', () {
final peers = List.generate(
25,
(index) => _peer(id: index.toString()),
)..insert(1, _peer(id: '0'));
final ids = autocompleteOnlineQueryIds(peers, limit: 20);
expect(ids, hasLength(20));
expect(ids.first, '0');
expect(ids.where((id) => id == '0'), hasLength(1));
expect(ids.last, '19');
});
test('empty online query ids cancel pending debounce', () async {
final queriedIds = <List<String>>[];
final loader = AllPeersLoader(
queryOnlines: (ids) async {
queriedIds.add(ids);
},
queryOnlineDebounce: Duration(milliseconds: 1),
);
loader.queryOnlines([_peer(id: '123456789')]);
loader.queryOnlines([]);
await Future.delayed(Duration(milliseconds: 2));
expect(queriedIds, isEmpty);
});
test('failed online query enqueue does not suppress retry', () async {
var queryCount = 0;
final loader = AllPeersLoader(
queryOnlines: (ids) {
queryCount += 1;
return Future<void>.error(Exception('queue full'));
},
queryOnlineDebounce: Duration(milliseconds: 1),
);
loader.queryOnlines([_peer(id: '123456789')]);
await Future.delayed(Duration(milliseconds: 2));
loader.queryOnlines([_peer(id: '123456789')]);
await Future.delayed(Duration(milliseconds: 2));
expect(queryCount, 2);
});
test('online callback updates currently displayed options', () async {
final loader = AllPeersLoader(
queryOnlines: (ids) async {},
queryOnlineDebounce: Duration(milliseconds: 1),
);
final displayedOptions = [_peer(id: '123456789')];
loader.queryOnlines(displayedOptions);
loader.updateOnlineStateForTesting({
'onlines': '123456789',
'offlines': '',
});
expect(displayedOptions.single.online, isTrue);
await Future.delayed(Duration(milliseconds: 2));
});
test('cached online callback state is reapplied after peers merge', () {
final loader = AllPeersLoader();
loader.updateOnlineStateForTesting({
'onlines': '123456789',
'offlines': '',
});
final mergedPeers = [_peer(id: '123456789')];
loader.applyLastOnlineStateForTesting(mergedPeers);
expect(mergedPeers.single.online, isTrue);
});
}
+63
View File
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
final testClients = [
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false),
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false),
Client(2, false, false, false, "UserC", "331123123", true, false, false),
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false,
false)
];
/// flutter run -d {platform} -t test/cm_demo.dart to test cm
void main() async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
await windowManager.setSize(const Size(400, 600));
await windowManager.setAlignment(Alignment.topRight);
await initEnv(kAppTypeMain);
for (var client in testClients) {
gFFI.serverModel.clients.add(client);
gFFI.serverModel.tabController.add(TabInfo(
key: client.id.toString(),
label: client.name,
closable: false,
page: buildConnectionCard(client)));
}
runApp(GetMaterialApp(
debugShowCheckedModeBanner: false,
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage()));
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
// ensure initial window size to be changed
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
await Future.wait([
windowManager.setAlignment(Alignment.topRight),
windowManager.focus(),
windowManager.setOpacity(1)
]);
// ensure
windowManager.setAlignment(Alignment.topRight);
});
}
+15 -57
View File
@@ -1,62 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_test/flutter_test.dart';
final testClients = [
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false),
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false),
Client(2, false, false, false, "UserC", "331123123", true, false, false, false),
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false)
];
import 'cm_demo.dart' as cm_demo;
/// flutter run -d {platform} -t test/cm_test.dart to test cm
void main(List<String> args) async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
await windowManager.setSize(const Size(400, 600));
await windowManager.setAlignment(Alignment.topRight);
await initEnv(kAppTypeMain);
for (var client in testClients) {
gFFI.serverModel.clients.add(client);
gFFI.serverModel.tabController.add(TabInfo(
key: client.id.toString(),
label: client.name,
closable: false,
page: buildConnectionCard(client)));
}
runApp(GetMaterialApp(
debugShowCheckedModeBanner: false,
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage()));
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
// ensure initial window size to be changed
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
await Future.wait([
windowManager.setAlignment(Alignment.topRight),
windowManager.focus(),
windowManager.setOpacity(1)
void main() {
test('connection manager demo clients match the current Client API', () {
expect(cm_demo.testClients, hasLength(4));
expect(cm_demo.testClients.map((client) => client.name), [
'UserAAAAAA',
'UserBBBBB',
'UserC',
'UserDDDDDDDDDDDd',
]);
// ensure
windowManager.setAlignment(Alignment.topRight);
expect(
cm_demo.testClients.every(
(client) => client.keyboard && !client.clipboard && !client.audio),
isTrue,
);
});
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
void main() {
testWidgets('server settings text fields preserve literal input',
(tester) async {
final controller = TextEditingController(text: 'AbCdR1c1E=');
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: serverSettingsTextFormField(
label: 'Key',
controller: controller,
errorMsg: '',
autofocus: true,
),
),
));
final textField = tester.widget<TextField>(find.byType(TextField));
expect(textField.controller, controller);
expect(textField.autofocus, isTrue);
expect(textField.keyboardType, TextInputType.visiblePassword);
expect(textField.textCapitalization, TextCapitalization.none);
expect(textField.autocorrect, isFalse);
expect(textField.enableSuggestions, isFalse);
expect(textField.smartDashesType, SmartDashesType.disabled);
expect(textField.smartQuotesType, SmartQuotesType.disabled);
expect(textField.enableIMEPersonalizedLearning, isFalse);
expect(
textField.spellCheckConfiguration,
const SpellCheckConfiguration.disabled(),
);
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.8"
edition = "2021"
description = "RustDesk Remote Desktop"
+10 -5
View File
@@ -2,6 +2,7 @@
import os
import optparse
import subprocess
from hashlib import md5
import brotli
import datetime
@@ -65,11 +66,15 @@ def write_app_metadata(output_folder: str):
print(f"App metadata has been written to {output_path}")
def build_portable(output_folder: str, target: str):
os.chdir(output_folder)
if target:
os.system("cargo build --locked --release --target " + target)
else:
os.system("cargo build --locked --release")
current_dir = os.getcwd()
try:
os.chdir(output_folder)
cmd = ["cargo", "build", "--locked", "--release"]
if target:
cmd.extend(["--target", target])
subprocess.run(cmd, check=True)
finally:
os.chdir(current_dir)
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
+1 -1
View File
@@ -47,7 +47,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
format!("{}-{}", target_arch, target_os)
}
} else if target_os == "windows" {
"x64-windows-static".to_owned()
format!("{}-windows-static", target_arch)
} else {
format!("{}-{}", target_arch, target_os)
};
+74 -17
View File
@@ -52,6 +52,33 @@ lazy_static::lazy_static! {
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
}
fn find_windows(cls: &str, name: &str) -> Result<Vec<HWND>> {
let name_c = CString::new(name)?;
let cls_c = if cls.is_empty() {
None
} else {
Some(CString::new(cls)?)
};
let mut hwnds = Vec::new();
unsafe {
let mut after = NULL as _;
loop {
let hwnd = FindWindowExA(
NULL as _,
after,
cls_c.as_ref().map_or(NULL as _, |c| c.as_ptr()),
name_c.as_ptr(),
);
if hwnd.is_null() {
break;
}
hwnds.push(hwnd);
after = hwnd;
}
}
Ok(hwnds)
}
pub type REFWICPixelFormatGUID = *const GUID;
pub type WICPixelFormatGUID = GUID;
@@ -247,6 +274,8 @@ pub struct CapturerMag {
rect: RECT,
width: usize,
height: usize,
excluded_window_target: Option<(String, String)>,
excluded_windows: Vec<HWND>,
}
impl Drop for CapturerMag {
@@ -261,6 +290,10 @@ impl CapturerMag {
MagInterface::new().is_ok()
}
// This captures through the legacy Windows Magnification API. Do not infer
// multi-monitor capture support from privacy overlay coverage: WebRTC also
// disables its magnifier capturer when SM_CMONITORS != 1.
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
unsafe {
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
@@ -305,6 +338,8 @@ impl CapturerMag {
},
width,
height,
excluded_window_target: None,
excluded_windows: Vec::new(),
};
unsafe {
@@ -436,19 +471,41 @@ impl CapturerMag {
}
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
let name_c = CString::new(name)?;
let mut hwnds = find_windows(cls, name)?;
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
self.excluded_window_target = Some((cls.to_owned(), name.to_owned()));
if hwnds.is_empty() {
self.excluded_windows.clear();
return Ok(false);
}
self.exclude_windows(&mut hwnds)?;
self.excluded_windows = hwnds;
Ok(true)
}
fn refresh_excluded_windows(&mut self) -> Result<()> {
let Some((cls, name)) = self.excluded_window_target.as_ref() else {
return Ok(());
};
let mut hwnds = find_windows(cls, name)?;
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
// This runs from frame() because refreshed privacy overlays get new
// HWNDs. It is only used on the legacy magnifier backend while privacy
// mode is active; if it shows up as hot-path cost, throttle this check.
// Keep the previous filter list while privacy windows are being recreated.
if hwnds.is_empty() || hwnds == self.excluded_windows {
return Ok(());
}
self.exclude_windows(&mut hwnds)?;
self.excluded_windows = hwnds;
Ok(())
}
fn exclude_windows(&mut self, hwnds: &mut [HWND]) -> Result<bool> {
let count = hwnds.len() as _;
unsafe {
let mut hwnd = if cls.len() == 0 {
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
} else {
let cls_c = CString::new(cls).unwrap();
FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr())
};
if hwnd.is_null() {
return Ok(false);
}
if let Some(set_window_filter_list_func) =
self.mag_interface.set_window_filter_list_func
{
@@ -456,16 +513,15 @@ impl CapturerMag {
== set_window_filter_list_func(
self.magnifier_window,
MW_FILTERMODE_EXCLUDE,
1,
&mut hwnd,
count,
hwnds.as_mut_ptr(),
)
{
return Err(Error::new(
ErrorKind::Other,
format!(
"Failed MagSetWindowFilterList for cls {} name {}, error {}",
cls,
name,
"Failed MagSetWindowFilterList for {} windows, error {}",
count,
Error::last_os_error()
),
));
@@ -496,6 +552,7 @@ impl CapturerMag {
}
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
self.refresh_excluded_windows()?;
Self::clear_data();
unsafe {
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.4.7
pkgver=1.4.8
pkgrel=0
epoch=
pkgdesc=""
+1 -1
View File
@@ -1,3 +1,3 @@
#! /usr/bin/env bash
sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
sed -i "s/\b$1\b/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
cargo run # to bump version in cargo lock
@@ -7,6 +7,10 @@
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<Keyword>Win32Proj</Keyword>
@@ -22,6 +26,12 @@
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
@@ -30,6 +40,9 @@
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
@@ -53,6 +66,28 @@
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;EXAMPLECADLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<AdditionalDependencies>msi.lib;version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Common.h" />
<ClInclude Include="framework.h" />
@@ -65,6 +100,7 @@
<ClCompile Include="FirewallRules.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ReadConfig.cpp" />
<ClCompile Include="RemotePrinter.cpp" />
+1 -1
View File
@@ -67,7 +67,7 @@
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
<!-- Remote printer also works on Win8.1 in my test. -->
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;" />
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND (PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;)" />
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT &gt;= 603" />
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
@@ -4,17 +4,17 @@
<?include ..\Includes.wxi?>
<!--
Properties and related actions for specifying whether to install start menu/desktop shortcuts.
Properties and related actions for specifying whether to install shortcuts and the printer.
-->
<!-- These are the actual properties that get used in conditions to determine whether to
install start menu shortcuts, they are initialized with a default value to install shortcuts.
They should not be set directly from the command line or registry, instead the CREATE* properties
below should be set, then they will update these properties with their values only if set. -->
install start menu shortcuts or the printer. Shortcut properties default to install;
PRINTER defaults to not install. The CREATE* properties below update shortcut
properties from command line, bundle, or registry values. -->
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="PRINTER" Value="1" Secure="yes"></Property>
<Property Id="PRINTER" Secure="yes"></Property>
<!-- These properties get set from either the command line, bundle or registry value,
if set they update the properties above with their value. -->
@@ -77,7 +77,11 @@
<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="PRINTER" Value="" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
<!-- PRINTER defaults to empty now, so a saved or command-line INSTALLPRINTER=1
must explicitly enable the main PRINTER property. Non-truthy INSTALLPRINTER
values still clear PRINTER so upgrades preserve an explicit disabled choice. -->
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;" />
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</Fragment>
</Wix>
+1 -1
View File
@@ -3,7 +3,7 @@
<IncludeSearchPaths>
</IncludeSearchPaths>
<Configurations>Release</Configurations>
<Platforms>x64</Platforms>
<Platforms>x64;ARM64</Platforms>
</PropertyGroup>
<ItemGroup>
<Content Include="Includes.wxi" />
+5
View File
@@ -10,12 +10,17 @@ EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|x64 = Release|x64
Release|ARM64 = Release|ARM64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.Build.0 = Release|x64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|ARM64.ActiveCfg = Release|ARM64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|ARM64.Build.0 = Release|ARM64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.ActiveCfg = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.Build.0 = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|ARM64.ActiveCfg = Release|ARM64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|ARM64.Build.0 = Release|ARM64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.8
Release: 0
Summary: RPM package
License: GPL-3.0
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.8
Release: 0
Summary: RPM package
License: GPL-3.0
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.8
Release: 0
Summary: RPM package
License: GPL-3.0
+12 -4
View File
@@ -130,14 +130,18 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
--cc=cl \
--enable-gpl \
--enable-d3d11va \
--enable-cuda \
--enable-ffnvcodec \
--enable-hwaccel=h264_nvdec \
--enable-hwaccel=hevc_nvdec \
--enable-hwaccel=h264_d3d11va \
--enable-hwaccel=hevc_d3d11va \
--enable-hwaccel=h264_d3d11va2 \
--enable-hwaccel=hevc_d3d11va2 \
")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86" OR VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
string(APPEND OPTIONS "\
--enable-cuda \
--enable-ffnvcodec \
--enable-hwaccel=h264_nvdec \
--enable-hwaccel=hevc_nvdec \
--enable-amf \
--enable-encoder=h264_amf \
--enable-encoder=hevc_amf \
@@ -147,6 +151,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
--enable-encoder=h264_qsv \
--enable-encoder=hevc_qsv \
")
endif()
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
set(LIB_MACHINE_ARG /machine:x86)
@@ -154,6 +159,9 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(LIB_MACHINE_ARG /machine:x64)
string(APPEND OPTIONS " --arch=x86_64")
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64")
set(LIB_MACHINE_ARG /machine:arm64)
string(APPEND OPTIONS " --arch=aarch64 --enable-cross-compile")
else()
message(FATAL_ERROR "Unsupported target architecture")
endif()
+43 -5
View File
@@ -96,6 +96,8 @@ pub mod screenshot;
pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30);
// Empirical restart reconnect grace window.
const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
pub const VIDEO_QUEUE_SIZE: usize = 120;
const MAX_DECODE_FAIL_COUNTER: usize = 3;
@@ -1740,7 +1742,10 @@ pub struct LoginConfigHandler {
features: Option<Features>,
pub session_id: u64, // used for local <-> server communication
pub supported_encoding: SupportedEncoding,
pub restarting_remote_device: bool,
restarting_remote_device: bool,
// Start time of the restart grace window. On Windows the peer may briefly
// reconnect before the real reboot disconnect.
restart_remote_device_at: Option<Instant>,
pub force_relay: bool,
pub direct: Option<bool>,
pub received: bool,
@@ -1849,7 +1854,7 @@ impl LoginConfigHandler {
}
self.session_id = sid;
self.supported_encoding = Default::default();
self.restarting_remote_device = false;
self.clear_restarting_remote_device();
self.force_relay =
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|| force_relay
@@ -2779,6 +2784,30 @@ impl LoginConfigHandler {
msg_out
}
pub fn mark_restarting_remote_device(&mut self) {
self.restarting_remote_device = true;
self.restart_remote_device_at = Some(Instant::now());
}
pub fn clear_restarting_remote_device(&mut self) {
self.restarting_remote_device = false;
self.restart_remote_device_at = None;
}
pub fn is_restarting_remote_device(&self) -> bool {
if !self.restarting_remote_device {
return false;
}
// Keep this flag alive for a short grace window instead of clearing it on
// connection_ready or the first peer bytes. During OS restart the peer can
// briefly reconnect before the real reboot disconnect, and clearing it too
// early would let the next disconnect escape the restart flow and fall back
// to the normal error dialog / manual reconnect path.
self.restart_remote_device_at
.map(|started_at| started_at.elapsed() < RESTART_REMOTE_DEVICE_GRACE)
.unwrap_or(false)
}
pub fn get_conn_token(&self) -> Option<String> {
if self.password.is_empty() {
return None;
@@ -3718,9 +3747,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn on_establish_connection_error(&self, err: String) {
let title = "Connection Error";
let text = err.to_string();
let lc = self.get_lch();
let direct = lc.read().unwrap().direct;
let received = lc.read().unwrap().received;
let lch = self.get_lch();
let (is_restarting, direct, received) = {
let lc = lch.read().unwrap();
(lc.is_restarting_remote_device(), lc.direct, lc.received)
};
if is_restarting {
log::info!("Restart remote device, suppress connection error: {err}");
// Flutter treats this as a reconnect control event. The text is kept
// for legacy UI and existing translation reuse.
self.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
return;
}
let mut relay_hint = false;
let mut relay_hint_type = "relay-hint";
+22 -4
View File
@@ -10,6 +10,10 @@ use crate::{
common::get_default_sound_input,
ui_session_interface::{InvokeUiSession, Session},
};
// Empirical no-data window before exposing the restart reconnect state to the UI.
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(feature = "unix-file-copy-paste")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
#[cfg(any(
@@ -153,7 +157,6 @@ impl<T: InvokeUiSession> Remote<T> {
}
};
let mut last_recv_time = Instant::now();
let mut received = false;
let conn_type = if self.handler.is_file_transfer() {
ConnType::FILE_TRANSFER
@@ -219,6 +222,7 @@ impl<T: InvokeUiSession> Remote<T> {
let mut fps_instant = Instant::now();
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
let mut last_recv_time = Instant::now();
loop {
tokio::select! {
@@ -244,7 +248,7 @@ impl<T: InvokeUiSession> Remote<T> {
} else {
if self.handler.is_restarting_remote_device() {
log::info!("Restart remote device");
self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", "");
self.handler.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
} else {
log::info!("Reset by the peer");
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
@@ -279,6 +283,12 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
_ = status_timer.tick() => {
if self.handler.is_restarting_remote_device()
&& last_recv_time.elapsed() >= RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT
{
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
break;
}
let elapsed = fps_instant.elapsed().as_millis();
if elapsed < 1000 {
continue;
@@ -1426,7 +1436,11 @@ impl<T: InvokeUiSession> Remote<T> {
self.handler.set_cursor_position(cp);
}
Some(message::Union::Clipboard(cb)) => {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
let clipboard_allowed = {
let lc = self.handler.lc.read().unwrap();
!lc.disable_clipboard.v && !lc.view_only.v
};
if clipboard_allowed {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(vec![cb], ClipboardSide::Client);
#[cfg(target_os = "ios")]
@@ -1445,7 +1459,11 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
Some(message::Union::MultiClipboards(_mcb)) => {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
let clipboard_allowed = {
let lc = self.handler.lc.read().unwrap();
!lc.disable_clipboard.v && !lc.view_only.v
};
if clipboard_allowed {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
#[cfg(target_os = "ios")]
+19
View File
@@ -868,6 +868,7 @@ pub mod clipboard_listener {
.unwrap()
.insert(name.clone(), tx);
cleanup_stale_listener(&mut listener_lock);
if listener_lock.handle.is_none() {
log::info!("Start clipboard listener thread");
let handler = Handler {
@@ -893,6 +894,24 @@ pub mod clipboard_listener {
Ok(())
}
fn cleanup_stale_listener(listener: &mut ClipboardListener) {
if !listener
.handle
.as_ref()
.map(|(_, h)| h.is_finished())
.unwrap_or(false)
{
return;
}
if let Some((shutdown, h)) = listener.handle.take() {
log::warn!("Cleaning up stale clipboard listener handle");
if let Err(e) = h.join() {
log::error!("Clipboard listener thread panicked during stale cleanup: {:?}", e);
}
drop(shutdown);
}
}
pub fn unsubscribe(name: &str) {
log::info!("Unsubscribe clipboard listener: {}", name);
let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap();
+20 -5
View File
@@ -262,11 +262,9 @@ pub fn core_main() -> Option<Vec<String>> {
if config::is_disable_installation() {
return None;
}
#[cfg(not(windows))]
let options = "desktopicon startmenu";
#[cfg(windows)]
let options = "desktopicon startmenu printer";
let res = platform::install_me(options, "".to_owned(), true, args.len() > 1);
let (printer_override, debug) = parse_silent_install_args(&args);
let options = platform::get_silent_install_options(printer_override);
let res = platform::install_me(options, "".to_owned(), true, debug);
let text = match res {
Ok(_) => translate("Installation Successful!".to_string()),
Err(err) => {
@@ -933,6 +931,23 @@ fn is_cli_setting_change_disabled() -> bool {
config::is_disable_settings() && !allow_command_line_settings
}
#[cfg(windows)]
fn parse_silent_install_args(args: &[String]) -> (Option<bool>, bool) {
let mut printer_override = None;
let mut debug = false;
for arg in args.iter().skip(1) {
match arg.as_str() {
"printer=1" => printer_override = Some(true),
"printer=0" => printer_override = Some(false),
"debug" => debug = true,
_ => {}
}
}
(printer_override, debug)
}
#[cfg(test)]
mod tests {
use super::*;
+4 -2
View File
@@ -326,12 +326,14 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
try_sync_peer_option(&session, &session_id, &value, None);
}
#[cfg(not(target_os = "ios"))]
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
if sessions::get_session_by_session_id(&session_id).is_some()
&& (value == "disable-clipboard" || value == "view-only")
{
crate::flutter::update_text_clipboard_required();
}
#[cfg(feature = "unix-file-copy-paste")]
if sessions::get_session_by_session_id(&session_id).is_some()
&& value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE
&& (value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only")
{
crate::flutter::update_file_clipboard_required();
}
+43
View File
@@ -1245,11 +1245,49 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec<KeyEv
#[inline]
pub fn map_keyboard_mode(_peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
if let Some(evt) = windows_peer_special_key(_peer, event) {
return vec![evt];
}
_map_keyboard_mode(_peer, event, key_event)
.map(|e| vec![e])
.unwrap_or_default()
}
fn windows_peer_special_key(peer: &str, event: &Event) -> Option<KeyEvent> {
if peer != OS_LOWER_WINDOWS {
return None;
}
let (key, down) = match event.event_type {
EventType::KeyPress(key) => (key, true),
EventType::KeyRelease(key) => (key, false),
_ => return None,
};
// Handle only `Pause` for Windows peers for now.
// Windows has no normal scan code for `Pause`, so send it as a legacy control key.
#[cfg(target_os = "windows")]
let is_pause = {
// The Windows scan code can look like `NumLock`; VK_PAUSE distinguishes it.
let pause_vk_code = rdev::win_code_from_key(Key::Pause);
key == Key::Pause || pause_vk_code == Some(event.platform_code as _)
};
#[cfg(not(target_os = "windows"))]
let is_pause = key == Key::Pause;
if !is_pause {
return None;
}
let mut key_event = KeyEvent::new();
key_event.mode = KeyboardMode::Legacy.into();
key_event.down = down;
key_event.set_control_key(ControlKey::Pause);
let (alt, ctrl, shift, command) = client::get_modifiers_state(false, false, false, false);
client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command);
Some(key_event)
}
fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option<KeyEvent> {
match event.event_type {
EventType::KeyPress(..) => {
@@ -1421,6 +1459,11 @@ fn is_press(event: &Event) -> bool {
pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
let mut events: Vec<KeyEvent> = Vec::new();
if let Some(evt) = windows_peer_special_key(peer, event) {
events.push(evt);
return events;
}
if let Some(unicode_info) = &event.unicode {
if unicode_info.is_dead {
#[cfg(target_os = "macos")]
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "إعادة تعيين اختيار إدخال لوحة المفاتيح"),
("remember-wayland-keyboard-choice-tip", "لا تسأل مرة أخرى لهذا الكمبيوتر البعيد"),
("Why this happens", "سبب حدوث ذلك"),
("Switch display", "تبديل الشاشة"),
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Скінуць выбар уводу з клавіятуры"),
("remember-wayland-keyboard-choice-tip", "Не пытацца зноў для гэтага аддаленага кампутара"),
("Why this happens", "Чаму гэта адбываецца"),
("Switch display", "Пераключыць дысплэй"),
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Нулиране на избора за въвеждане от клавиатура"),
("remember-wayland-keyboard-choice-tip", "Не питай отново за този отдалечен компютър"),
("Why this happens", "Защо се случва това"),
("Switch display", "Превключване на дисплея"),
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Restableix l'opció d'entrada de teclat"),
("remember-wayland-keyboard-choice-tip", "No tornis a preguntar-ho per a aquest equip remot"),
("Why this happens", "Per què passa això"),
("Switch display", "Canvia de pantalla"),
("Show monitor switch button on the main toolbar", "Mostra el botó de canvi de monitor a la barra deines principal"),
("Show on the minimized toolbar", "Mostra a la barra deines minimitzada"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
("Why this happens", "了解原因"),
("Switch display", "切换显示器"),
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
("Show on the minimized toolbar", "在最小化工具栏上显示"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Resetovat volbu vstupu z klávesnice"),
("remember-wayland-keyboard-choice-tip", "Pro tento vzdálený počítač se již neptat"),
("Why this happens", "Proč k tomu dochází"),
("Switch display", "Přepnout obrazovku"),
("Show monitor switch button on the main toolbar", "Zobrazit tlačítko přepnutí monitoru na hlavním panelu nástrojů"),
("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Nulstil valg for tastaturinput"),
("remember-wayland-keyboard-choice-tip", "Spørg ikke igen for denne fjerncomputer"),
("Why this happens", "Hvorfor dette sker"),
("Switch display", "Skift skærm"),
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"),
("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Auswahl der Tastatureingabe zurücksetzen"),
("remember-wayland-keyboard-choice-tip", "Für diesen entfernten Computer nicht erneut fragen"),
("Why this happens", "Warum dies passiert"),
("Switch display", "Anzeige wechseln"),
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"),
("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Επαναφορά επιλογής εισαγωγής από πληκτρολόγιο"),
("remember-wayland-keyboard-choice-tip", "Να μην ερωτηθώ ξανά για αυτόν τον απομακρυσμένο υπολογιστή"),
("Why this happens", "Γιατί συμβαίνει αυτό"),
("Switch display", "Εναλλαγή οθόνης"),
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Restarigi la elekton de klavara enigo"),
("remember-wayland-keyboard-choice-tip", "Ne demandi denove por ĉi tiu fora komputilo"),
("Why this happens", "Kial ĉi tio okazas"),
("Switch display", "Ŝalti ekranon"),
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"),
("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Restablecer la opción de entrada del teclado"),
("remember-wayland-keyboard-choice-tip", "No volver a preguntar para este equipo remoto"),
("Why this happens", "Por qué ocurre esto"),
("Switch display", "Cambiar de pantalla"),
("Show monitor switch button on the main toolbar", "Mostrar el botón de cambio de monitor en la barra de herramientas principal"),
("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Lähtesta klaviatuurisisestuse valik"),
("remember-wayland-keyboard-choice-tip", "Ära küsi selle kaugarvuti puhul uuesti"),
("Why this happens", "Miks see juhtub"),
("Switch display", "Vaheta kuva"),
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"),
("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Berrezarri teklatuko sarreraren aukera"),
("remember-wayland-keyboard-choice-tip", "Ez galdetu berriro urruneko ordenagailu honetarako"),
("Why this happens", "Zergatik gertatzen den hau"),
("Switch display", "Aldatu pantaila"),
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "بازنشانی انتخاب ورودی صفحه کلید"),
("remember-wayland-keyboard-choice-tip", "برای این رایانه از راه دور دوباره نپرس"),
("Why this happens", "چرا این اتفاق می‌افتد"),
("Switch display", "تعویض نمایشگر"),
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچک‌شده"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Nollaa näppäimistösyötteen valinta"),
("remember-wayland-keyboard-choice-tip", "Älä kysy uudelleen tältä etätietokoneelta"),
("Why this happens", "Miksi näin tapahtuu"),
("Switch display", "Vaihda näyttöä"),
("Show monitor switch button on the main toolbar", "Näytä näytön vaihtopainike päätyökalurivillä"),
("Show on the minimized toolbar", "Näytä pienennetyssä työkalurivissä"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Réinitialiser le choix de la saisie au clavier"),
("remember-wayland-keyboard-choice-tip", "Ne plus demander pour cet appareil distant"),
("Why this happens", "Pourquoi cela se produit"),
("Switch display", "Changer d’écran"),
("Show monitor switch button on the main toolbar", "Afficher le bouton de changement d’écran dans la barre doutils principale"),
("Show on the minimized toolbar", "Afficher dans la barre doutils réduite"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "კლავიატურის შეყვანის არჩევანის ჩამოყრა"),
("remember-wayland-keyboard-choice-tip", "აღარ მკითხო ამ დისტანციური კომპიუტერისთვის"),
("Why this happens", "რატომ ხდება ეს"),
("Switch display", "ეკრანის გადართვა"),
("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"),
("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "કીબોર્ડ ઇનપુટ પસંદગી રિસેટ કરો"),
("remember-wayland-keyboard-choice-tip", "આ રિમોટ કમ્પ્યુટર માટે ફરીથી પૂછશો નહીં"),
("Why this happens", "આવું શા માટે થાય છે"),
("Switch display", "ડિસ્પ્લે બદલો"),
("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"),
("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "אפס את בחירת קלט המקלדת"),
("remember-wayland-keyboard-choice-tip", "אל תשאל שוב עבור מחשב מרוחק זה"),
("Why this happens", "מדוע זה קורה"),
("Switch display", "החלפת צג"),
("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"),
("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "कीबोर्ड इनपुट चयन रीसेट करें"),
("remember-wayland-keyboard-choice-tip", "इस रिमोट कंप्यूटर के लिए दोबारा न पूछें"),
("Why this happens", "ऐसा क्यों होता है"),
("Switch display", "डिस्प्ले बदलें"),
("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"),
("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Poništi izbor unosa tipkovnicom"),
("remember-wayland-keyboard-choice-tip", "Ne pitaj ponovno za ovo udaljeno računalo"),
("Why this happens", "Zašto se ovo događa"),
("Switch display", "Promijeni zaslon"),
("Show monitor switch button on the main toolbar", "Prikaži gumb za prebacivanje monitora na glavnoj alatnoj traci"),
("Show on the minimized toolbar", "Prikaži na minimiziranoj alatnoj traci"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Billentyűzetbevitel választásának visszaállítása"),
("remember-wayland-keyboard-choice-tip", "Ne kérdezze meg újra ennél a távoli számítógépnél"),
("Why this happens", "Miért történik ez"),
("Switch display", "Kijelző váltása"),
("Show monitor switch button on the main toolbar", "Monitorváltó gomb megjelenítése a fő eszköztáron"),
("Show on the minimized toolbar", "Megjelenítés a kis méretű eszköztáron"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Setel ulang pilihan masukan keyboard"),
("remember-wayland-keyboard-choice-tip", "Jangan tanya lagi untuk komputer jarak jauh ini"),
("Why this happens", "Mengapa ini terjadi"),
("Switch display", "Ganti tampilan"),
("Show monitor switch button on the main toolbar", "Tampilkan tombol pengalih monitor di bilah alat utama"),
("Show on the minimized toolbar", "Tampilkan di bilah alat yang diperkecil"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Ripristina scelta input da tastiera"),
("remember-wayland-keyboard-choice-tip", "Non chiedere più per questo computer remoto"),
("Why this happens", "Perché accade questo"),
("Switch display", "Cambia schermo"),
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio schermo"),
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
].iter().cloned().collect();
}
+23 -20
View File
@@ -197,9 +197,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Please enter the folder name", "フォルダー名を入力してください"),
("Fix it", "修復する"),
("Warning", "警告"),
("Login screen using Wayland is not supported", "Wayland を使用したログインスクリーンはサポートされていません"),
("Login screen using Wayland is not supported", "Wayland を使用したログイン画面は対応していません"),
("Reboot required", "再起動が必要です"),
("Unsupported display server", "サポートされていないディスプレイサーバー"),
("Unsupported display server", "非対応のディスプレイサーバー"),
("x11 expected", "X11 が必要です"),
("Port", "ポート"),
("Settings", "設定"),
@@ -268,11 +268,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Share screen", "画面を共有"),
("Chat", "チャット"),
("Total", "合計"),
("items", "個のアイテム"),
("items", "個の項目"),
("Selected", "選択済み"),
("Screen Capture", "画面キャプチャ"),
("Screen Capture", "画面キャプチャ"),
("Input Control", "入力操作"),
("Audio Capture", "音声キャプチャ"),
("Audio Capture", "オーディオをキャプチャ"),
("Do you accept?", "許可しますか?"),
("Open System Setting", "システム設定を開く"),
("How to get Android input permission?", "Android の入力権限を取得するには?"),
@@ -281,7 +281,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_new_connection_tip", "新しい操作リクエストが届きました。この端末を操作しようとしています。"),
("android_service_will_start_tip", "「画面キャプチャ」を有効にするとサービスが自動的に開始され、他の端末がこの端末への接続をリクエストできるようになります。"),
("android_stop_service_tip", "サービスを停止すると、自動的に現在のセッションがすべて閉じられます。"),
("android_version_audio_tip", "現在の Android バージョンでは音声キャプチャはサポートされていません。Android 10 以降に更新してください。"),
("android_version_audio_tip", "使用している Android はオーディオキャプチャに対応していません。Android 10 以降に更新してください。"),
("android_start_service_tip", "「サービスを開始」をタップするか、「画面キャプチャ」の許可を有効にすると、画面共有サービスが開始されます。"),
("android_permission_may_not_change_tip", "権限の変更は現在のセッションには適用されません。再接続後に適用されます。"),
("Account", "アカウント"),
@@ -292,7 +292,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed", "失敗"),
("Succeeded", "成功"),
("Someone turns on privacy mode, exit", "プライバシーモードがオンになりました。終了します。"),
("Unsupported", "サポートされていません"),
("Unsupported", "対応していません"),
("Peer denied", "リモートホストに拒否されました"),
("Please install plugins", "プラグインをインストールしてください"),
("Peer exit", "リモートホストが退出しました"),
@@ -376,10 +376,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Confirm before closing multiple tabs", "複数のタブを閉じる前に確認する"),
("Keyboard Settings", "キーボードの設定"),
("Full Access", "フルアクセス"),
("Screen Share", "画面共有"),
("Screen Share", "画面共有"),
("ubuntu-21-04-required", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"),
("wayland-requires-higher-linux-version", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"),
("xdp-portal-unavailable", "Wayland の画面キャプチャに失敗しました。XDG Desktop Portal がクラッシュしたか、利用できない可能性があります。`systemctl --user restart xdg-desktop-portal` で再起動してみてください。"),
("xdp-portal-unavailable", "Wayland の画面キャプチャに失敗しました。XDG デスクトップポータルがクラッシュしたか、利用できない可能性があります。`systemctl --user restart xdg-desktop-portal` で再起動してみてください。"),
("JumpLink", "表示"),
("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(リモートコンピューターが操作します)"),
("Show RustDesk", "RustDesk を表示"),
@@ -397,7 +397,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Request access to your device", "デバイスへのアクセス要求"),
("Hide connection management window", "接続管理画面を隠す"),
("hide_cm_tip", "パスワードによるセッションを許可し、固定パスワードを使用する場合にのみ、管理画面の非表示を許可する。"),
("wayland_experiment_tip", "Wayland のサポートは試験的なものです。無人アクセスを使用する場合はX11デスクトップをご利用ください。"),
("wayland_experiment_tip", "Wayland の対応は試験的なものです。無人アクセスを使用する場合はX11デスクトップをご利用ください。"),
("Right click to select tabs", "右クリックでタブを選択"),
("Skipped", "スキップ"),
("Add to address book", "アドレス帳に追加"),
@@ -568,7 +568,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input_source_1_tip", "入力ソース 1"),
("input_source_2_tip", "入力ソース 2"),
("Swap control-command key", "ctrl と command キーを入れ替える"),
("swap-left-right-mouse", "マウスクリックを入れ替える"),
("swap-left-right-mouse", "マウスクリックを入れ替える"),
("2FA code", "二要素認証コード"),
("More", "詳細"),
("enable-2fa-title", "二要素認証を有効化する"),
@@ -601,10 +601,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("default_proxy_tip", "既定のプロトコルとポートは Socks5 と 1080 です。"),
("no_audio_input_device_tip", "オーディオ入力デバイスが見つかりません。"),
("Incoming", "受信"),
("Outgoing", ""),
("Clear Wayland screen selection", "Wayland の画面選択をクリア"),
("clear_Wayland_screen_selection_tip", "画面選択をクリア後、共有画面を再び選択できます。"),
("confirm_clear_Wayland_screen_selection_tip", "本当に Wayland の画面選択をクリアしますか?"),
("Outgoing", ""),
("Clear Wayland screen selection", "Wayland の画面選択を消去"),
("clear_Wayland_screen_selection_tip", "画面選択を消去後、共有画面を再び選択できます。"),
("confirm_clear_Wayland_screen_selection_tip", "本当に Wayland の画面選択を消去しますか?"),
("android_new_voice_call_tip", "新しい音声通話リクエストを受信しました。承認すると音声通話に切り替わります。"),
("texture_render_tip", "テクスチャレンダリングを使用し、画像をより滑らかに描画します。レンダリングの問題が発生した場合は無効にしてみてください。"),
("Use texture rendering", "テクスチャレンダリングを使用する"),
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "コントロールをされる側では一方向のファイル転送が有効になります。"),
("Authentication Required", "認証が必要です"),
("Authenticate", "認証"),
("web_id_input_tip", "同じサーバー内の ID を入力できます。Web クライアントでは直接 IP アドレスによるアクセスはサポートされていません。\n別のサーバー上のデバイスにアクセスする場合は、サーバーアドレス (<id>@<server_address>?key=<key_value>) を入力してください。\n 例: 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\nパブリックサーバー上のデバイスにアクセスする場合は、「<id>@public」と入力してください。パブリックサーバーはキーは不要です。"),
("web_id_input_tip", "同じサーバー内の ID を入力できます。Web クライアントでは IP アドレスによる直接アクセスに対応していません。\n別のサーバー上のデバイスにアクセスする場合は、サーバーアドレス (<id>@<server_address>?key=<key_value>) を入力してください。\n 例: 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\nパブリックサーバー上のデバイスにアクセスする場合は、「<id>@public」と入力してください。パブリックサーバーはキーは不要です。"),
("Download", "ダウンロード"),
("Upload folder", "フォルダーをアップロード"),
("Upload files", "ファイルをアップロード"),
@@ -674,7 +674,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("dont-show-again-tip", "今後は表示しない"),
("Take screenshot", "スクリーンショットを撮影"),
("Taking screenshot", "スクリーンショットを撮影中"),
("screenshot-merged-screen-not-supported-tip", "複数のディスプレイのスクリーンショットの結合は、現在サポートされていません。単一のディスプレイに切り替えてもう一度お試しください。"),
("screenshot-merged-screen-not-supported-tip", "複数のディスプレイのスクリーンショットの結合は、現在非対応です。単一のディスプレイに切り替えてもう一度お試しください。"),
("screenshot-action-tip", "スクリーンショットを続行する方法を選択してください。"),
("Save as", "保存先"),
("Copy to clipboard", "クリップボードにコピー"),
@@ -685,7 +685,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("download-new-version-failed-tip", "ダウンロードに失敗しました。もう一度お試しいただくか、「ダウンロード」ボタンをクリックしてリリースページからダウンロードし、手動でアップグレードしてください。"),
("Auto update", "ソフトウェアの自動更新を行う"),
("update-failed-check-msi-tip", "インストール方法の確認に失敗しました。「ダウンロード」ボタンをクリックしてリリースページからダウンロードし、手動でアップグレードしてください。"),
("websocket_tip", "WebSocket を使用する場合、リレー接続のみがサポートされます。"),
("websocket_tip", "WebSocket を使用する場合、リレー接続のみ対応しています。"),
("Use WebSocket", "WebSocket を使用する"),
("Trackpad speed", "トラックパッドの速度"),
("Default trackpad speed", "既定のトラックパッドの速度"),
@@ -695,7 +695,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("View camera", "カメラを表示"),
("Enable camera", "カメラを有効化する"),
("No cameras", "カメラなし"),
("view_camera_unsupported_tip", "リモートデバイスはカメラの表示をサポートしていません"),
("view_camera_unsupported_tip", "リモートデバイスはカメラの表示に対応していません"),
("Terminal", "ターミナル"),
("Enable terminal", "ターミナルを有効化する"),
("New tab", "新しいタブ"),
@@ -706,7 +706,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incorrect username or password.", "ユーザー名またはパスワードが正しくありません。"),
("The user is not an administrator.", "このユーザーは管理者ではありません。"),
("Failed to check if the user is an administrator.", "ユーザーが管理者であるかどうかを確認できませんでした。"),
("Supported only in the installed version.", "インストールされたバージョンでのみサポートされます。"),
("Supported only in the installed version.", "インストールされたバージョンでのみ対応しています。"),
("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"),
("Preparing for installation ...", "インストールの準備中です..."),
("Show my cursor", "自分のカーソルを表示する"),
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "キーボード入力の選択をリセット"),
("remember-wayland-keyboard-choice-tip", "このリモートコンピューターでは今後確認しない"),
("Why this happens", "この問題が起こる理由"),
("Switch display", "ディスプレイを切り替え"),
("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"),
("Show on the minimized toolbar", "最小化したツールバーに表示"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 초기화"),
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않기"),
("Why this happens", "이런 현상이 발생하는 이유"),
("Switch display", "디스플레이 전환"),
("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"),
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Пернетақта еңгізу таңдауын қалпына келтіру"),
("remember-wayland-keyboard-choice-tip", "Осы қашықтағы компьютер үшін қайта сұрамау"),
("Why this happens", "Бұл неге болады"),
("Switch display", "Дисплейді ауыстыру"),
("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"),
("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Atstatyti klaviatūros įvesties pasirinkimą"),
("remember-wayland-keyboard-choice-tip", "Daugiau neklausti dėl šio nuotolinio kompiuterio"),
("Why this happens", "Kodėl taip nutinka"),
("Switch display", "Perjungti ekraną"),
("Show monitor switch button on the main toolbar", "Rodyti monitoriaus perjungimo mygtuką pagrindinėje įrankių juostoje"),
("Show on the minimized toolbar", "Rodyti sumažintoje įrankių juostoje"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Atiestatīt tastatūras ievades izvēli"),
("remember-wayland-keyboard-choice-tip", "Vairs nejautāt par šo attālo datoru"),
("Why this happens", "Kāpēc tas notiek"),
("Switch display", "Pārslēgt displeju"),
("Show monitor switch button on the main toolbar", "Rādīt monitora pārslēgšanas pogu galvenajā rīkjoslā"),
("Show on the minimized toolbar", "Rādīt minimizētajā rīkjoslā"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "കീബോർഡ് ഇൻപുട്ട് തിരഞ്ഞെടുപ്പ് റീസെറ്റ് ചെയ്യുക"),
("remember-wayland-keyboard-choice-tip", "ഈ റിമോട്ട് കമ്പ്യൂട്ടറിനായി ഇനി ചോദിക്കരുത്"),
("Why this happens", "ഇത് എന്തുകൊണ്ട് സംഭവിക്കുന്നു"),
("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"),
("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"),
("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Tilbakestill valg for tastaturinndata"),
("remember-wayland-keyboard-choice-tip", "Ikke spør igjen for denne eksterne datamaskinen"),
("Why this happens", "Hvorfor dette skjer"),
("Switch display", "Bytt skjerm"),
("Show monitor switch button on the main toolbar", "Vis knapp for skjermbytte på hovedverktøylinjen"),
("Show on the minimized toolbar", "Vis på den minimerte verktøylinjen"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Keuze voor toetsenbordinvoer opnieuw instellen"),
("remember-wayland-keyboard-choice-tip", "Niet meer vragen voor deze externe computer"),
("Why this happens", "Waarom dit gebeurt"),
("Switch display", "Beeldscherm wisselen"),
("Show monitor switch button on the main toolbar", "Knop voor monitorwisseling weergeven op de hoofdwerkbalk"),
("Show on the minimized toolbar", "Weergeven op de geminimaliseerde werkbalk"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Zresetuj wybór dotyczący wprowadzania z klawiatury"),
("remember-wayland-keyboard-choice-tip", "Nie pytaj ponownie dla tego zdalnego komputera"),
("Why this happens", "Dlaczego tak się dzieje"),
("Switch display", "Przełącz ekran"),
("Show monitor switch button on the main toolbar", "Pokaż przycisk przełączania monitora na głównym pasku narzędzi"),
("Show on the minimized toolbar", "Pokaż na zminimalizowanym pasku narzędzi"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Repor escolha de entrada de teclado"),
("remember-wayland-keyboard-choice-tip", "Não voltar a perguntar para este computador remoto"),
("Why this happens", "Porque é que isto acontece"),
("Switch display", "Trocar de ecrã"),
("Show monitor switch button on the main toolbar", "Mostrar o botão de troca de monitor na barra de ferramentas principal"),
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
].iter().cloned().collect();
}
+21 -18
View File
@@ -16,18 +16,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Control Remote Desktop", "Controle um Computador Remoto"),
("Transfer file", "Transferir arquivos"),
("Connect", "Conectar"),
("Recent sessions", "Sessões Recentes"),
("Address book", "Lista de Endereços"),
("Recent sessions", "Sessões recentes"),
("Address book", "Lista de endereços"),
("Confirmation", "Confirmação"),
("TCP tunneling", "Tunelamento TCP"),
("Remove", "Remover"),
("Refresh random password", "Atualizar senha aleatória"),
("Set your own password", "Configure sua própria senha"),
("Refresh random password", "Gerar nova senha aleatória"),
("Set your own password", "Definir sua própria senha"),
("Enable keyboard/mouse", "Habilitar teclado/mouse"),
("Enable clipboard", "Habilitar Área de Transferência"),
("Enable file transfer", "Habilitar Transferência de Arquivos"),
("Enable TCP tunneling", "Habilitar Tunelamento TCP"),
("IP Whitelisting", "Lista de IPs Confiáveis"),
("Enable clipboard", "Habilitar área de transferência"),
("Enable file transfer", "Habilitar transferência de arquivos"),
("Enable TCP tunneling", "Habilitar tunelamento TCP"),
("IP Whitelisting", "Lista de IPs Permitidos"),
("ID/Relay Server", "Servidor ID/Relay"),
("Import server config", "Importar Configuração do Servidor"),
("Export Server Config", "Exportar Configuração do Servidor"),
@@ -320,12 +320,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Exit Fullscreen", "Sair da Tela Cheia"),
("Fullscreen", "Tela Cheia"),
("Mobile Actions", "Ações móveis"),
("Select Monitor", "Selecionar monitor"),
("Select Monitor", "Selecionar tela"),
("Control Actions", "Controlar ações"),
("Display Settings", "Configurações de exibição"),
("Ratio", "Proporção"),
("Image Quality", "Qualidade de imagem"),
("Scroll Style", "Estilo de Rolagem"),
("Scroll Style", "Estilo de rolagem"),
("Show Toolbar", "Mostrar barra de ferramentas"),
("Hide Toolbar", "Ocultar barra de ferramentas"),
("Direct Connection", "Conexão Direta"),
@@ -353,7 +353,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disconnect all devices?", "Desconectar todos os dispositivos?"),
("Clear", "Limpar"),
("Audio Input Device", "Dispositivo de entrada de áudio"),
("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"),
("Use IP Whitelisting", "Utilizar lista de IPs permitidos"),
("Network", "Rede"),
("Pin Toolbar", "Fixar barra de ferramentas"),
("Unpin Toolbar", "Desafixar barra de ferramentas"),
@@ -430,7 +430,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Strong", "Forte"),
("Switch Sides", "Trocar de lado"),
("Please confirm if you want to share your desktop?", "Por favor, confirme se você deseja compartilhar sua área de trabalho?"),
("Display", "Display"),
("Display", "Exibição"),
("Default View Style", "Estilo de Visualização Padrão"),
("Default Scroll Style", "Estilo de Rolagem Padrão"),
("Default Image Quality", "Qualidade de Imagem Padrão"),
@@ -463,7 +463,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty Password", "Senha Vazia"),
("Me", "Eu"),
("identical_file_tip", "Este arquivo é idêntico ao do parceiro."),
("show_monitors_tip", "Mostrar monitores na barra de ferramentas"),
("show_monitors_tip", "Mostrar telas na barra de ferramentas"),
("View Mode", "Modo de visualização"),
("login_linux_tip", "Você precisa fazer login na conta Linux remota para habilitar uma sessão de desktop X"),
("verify_rustdesk_password_tip", "Verifique a senha do RustDesk"),
@@ -674,7 +674,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("dont-show-again-tip", "Não mostrar novamente"),
("Take screenshot", "Capturar tela"),
("Taking screenshot", "Capturando tela"),
("screenshot-merged-screen-not-supported-tip", "Mesclar a captura de tela de múltiplos monitores não é suportada no momento. Por favor, alterne para um único monitor e tente novamente."),
("screenshot-merged-screen-not-supported-tip", "A captura de tela de múltiplas telas não é suportada no momento. Por favor, alterne para uma única tela e tente novamente."),
("screenshot-action-tip", "Por favor, selecione como deseja continuar com a captura de tela."),
("Save as", "Salvar como"),
("Copy to clipboard", "Copiar para área de transferência"),
@@ -693,11 +693,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
("Enable UDP hole punching", "Habilitar UDP hole punching"),
("View camera", "Visualizar câmera"),
("Enable camera", "Ativar câmera"),
("No cameras", "Sem câmeras"),
("Enable camera", "Habilitar câmera"),
("No cameras", "Nenhuma câmera"),
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
("Terminal", "Terminal"),
("Enable terminal", "Habilitar Terminal"),
("Enable terminal", "Habilitar terminal"),
("New tab", "Nova aba"),
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
@@ -744,7 +744,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "A senha permanente está definida como (oculta)."),
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
("Enable privacy mode", "Habilitar modo de privacidade"),
("allow-remote-toolbar-docking-any-edge", "Permitir fixar a barra de ferramentas remota em qualquer borda da janela"),
("allow-remote-toolbar-docking-any-edge", "Fixar a barra de ferramentas remota em qualquer borda da janela"),
("API Token", "Token de API"),
("Deploy", "Implantar"),
("Custom ID (optional)", "ID personalizado (opcional)"),
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Redefinir escolha de entrada do teclado"),
("remember-wayland-keyboard-choice-tip", "Não perguntar novamente para este computador remoto"),
("Why this happens", "Por que isso acontece"),
("Switch display", "Trocar de tela"),
("Show monitor switch button on the main toolbar", "Mostrar botão de troca de tela na barra de ferramentas"),
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Resetează alegerea pentru introducerea de la tastatură"),
("remember-wayland-keyboard-choice-tip", "Nu mai întreba pentru acest computer la distanță"),
("Why this happens", "De ce se întâmplă acest lucru"),
("Switch display", "Comută afișajul"),
("Show monitor switch button on the main toolbar", "Afișează butonul de comutare a monitorului în bara de instrumente principală"),
("Show on the minimized toolbar", "Afișează în bara de instrumente minimizată"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Сбросить выбор для ввода с клавиатуры"),
("remember-wayland-keyboard-choice-tip", "Больше не спрашивать для этого удалённого компьютера"),
("Why this happens", "Почему это происходит"),
("Switch display", "Переключить дисплей"),
("Show monitor switch button on the main toolbar", "Показывать кнопку переключения монитора на главной панели инструментов"),
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Reseta s'isseberada de s'insertada cun su tecladu"),
("remember-wayland-keyboard-choice-tip", "No torres a preguntare pro custu elaboradore remotu"),
("Why this happens", "Pro ite custu càpitat"),
("Switch display", "Càmbia ischermu"),
("Show monitor switch button on the main toolbar", "Mustra su butone de càmbiu de monitor in sa barra de aina printzipale"),
("Show on the minimized toolbar", "Mustra in sa barra de aina minimizada"),
].iter().cloned().collect();
}
+3
View File
@@ -758,5 +758,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-keyboard-input-reset-choice-tip", "Obnoviť voľbu vstupu z klávesnice"),
("remember-wayland-keyboard-choice-tip", "Nepýtať sa znova pre tento vzdialený počítač"),
("Why this happens", "Prečo sa to deje"),
("Switch display", "Prepnúť obrazovku"),
("Show monitor switch button on the main toolbar", "Zobraziť tlačidlo prepnutia monitora na hlavnom paneli nástrojov"),
("Show on the minimized toolbar", "Zobraziť na minimalizovanom paneli nástrojov"),
].iter().cloned().collect();
}

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