Compare commits

...

45 Commits

Author SHA1 Message Date
rustdesk f6212e6465 hbb_common 2026-07-14 14:54:11 +08:00
rustdesk 4c87c16b19 id whitelist 2026-07-14 14:39:33 +08:00
Maison da Silva fa418cace6 Translate 'Continue' to 'Continuar' in ptbr.rs (#15567)
Translate 'Continue' to 'Continuar' in ptbr.rs
2026-07-13 18:56:41 +08:00
rustdesk 94a2a2bb4a fix https://github.com/rustdesk/rustdesk/issues/15566 2026-07-11 21:43:04 +08:00
Maison da Silva 865fe71c46 Update Portuguese translations for clarity (#15534) 2026-07-11 17:18:12 +08:00
rustdesk 137298e05a revert back 2026-07-10 15:21:48 +08:00
rustdesk 685a89a171 target Android 15 2026-07-10 15:12:06 +08:00
RustDesk 12b5cc7f72 Revert "fix: ci: macos: allow signed but not notarized dmg (#15530)" (#15551)
This reverts commit 29e1852a68.
2026-07-10 11:47:13 +08:00
Vasyl Gello 480e9e8234 Fix default Android API version mismatch between vcpkg and rest of build (for working on android 6) (#14850)
* Fork vcpkg triplets to keep Android API version on 21

Fixes crash on API platforms 21 to 23 due to missing
symbol `__write_chk` (available since API 24).

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* flutter/build_android_deps.sh: Refactor to remove unused

... variables and shellcheck warnings.

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2026-07-10 11:38:43 +08:00
Zhenyu FU 29e1852a68 fix: ci: macos: allow signed but not notarized dmg (#15530)
* fix: ci: macos: allow signed but not notarized dmg
Signed-off-by: Zhenyu FU <ysfcore@outlook.com>

* fix: ci: macos: add pre-check for macos identity
Signed-off-by: Zhenyu FU <ysfcore@outlook.com>

* merge notarize checking to existing steps
Signed-off-by: Zhenyu FU <ysfcore@outlook.com>
2026-07-09 15:23:43 +08:00
fufesou 8314335b31 fix: update download, force tls (#15529)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-09 15:05:49 +08:00
bovirus acb9f63e1d Update it.rs (#15531) 2026-07-08 16:35:34 +08:00
VenusGirl❤ 005a8b4a04 Update Korean (#15525)
Updated Korean translations for clarity and accuracy.
2026-07-08 11:09:39 +08:00
RustDesk e2149974cc harden wf_cliprdr.c (#15515)
* harden wf_cliprdr.c

* fix copilot review

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix review

* fix review

* condense hardening comments, fix style in wf_cliprdr.c

Comment-only cleanup of the review-justification comments; also move
the mutex wait result declaration to the top of the block and fix
continuation-line indentation. No behavior change.

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

* add invariant tests for file contents request/response hardening

Cover the zeroed optional request fields, stream ID filtering,
oversized/NULL response rejection and the zero-byte EOF path.

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

* address copilot review findings in wf_cliprdr.c

- Reject a negative FILECONTENTS_SIZE result: m_lSize is unsigned, so a
  negative value became a huge bogus stream size that keeps reads going.
- Use a unique per-stream counter as the CLIPRDR streamId instead of a
  truncated IStream pointer, which could collide or be reused after free
  (and leaked heap addresses to the peer).
- Add req_f_request_mutex to serialize whole file-contents request/response
  cycles, enforcing the previously assumed one-outstanding-request
  invariant when multiple streams are read concurrently. Bounded acquire
  so a wedged request fails the read instead of hanging a consumer.

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

* serialize file-contents request state and poison streams after timeout

- Extract lock_mutex() for the WAIT_OBJECT_0/WAIT_ABANDONED idiom shared by
  take_req_fdata, the request-serialization acquire, and the response handler.
- Collapse the acquire/send/take/release cycle into
  cliprdr_request_filecontents_sync(), used by CliprdrStream_Read and the size
  probe in CliprdrStream_New.
- Publish req_f_stream_id_expected/req_f_size_requested under req_f_mutex in the
  sender and read them under the same lock in the response handler, removing the
  cross-thread data race on those fields.
- Poison a stream (m_failed) after a request fails/times out, so a late response
  carrying a previous offset's bytes cannot satisfy a later same-stream read.

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

* key the responder stream cache on connID as well as streamId

Per-stream ids restart from 1 in each peer process, so two connections can
emit the same streamId. The process-static pStreamStc cache keyed only on
streamId could then serve one peer the IStream cached for another peer (a
different file), silently returning wrong-file bytes. Add connID to the key.

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

* harden the format-data path against late/duplicate responses

The format-data rendezvous had the same single-slot race the file-contents
path just fixed: the channel thread rewrote clipboard->hmem with no lock while
explorer-thread consumers read/freed it, nothing serialized concurrent
requests, and no flag told an expected response from a stray one.

- Add format_request_mutex (serializes the whole request/response cycle) and
  hmem_mutex (guards the hmem hand-off and formatDataRespExpected).
- cliprdr_send_data_request now takes ownership of the response buffer under
  hmem_mutex and returns it to the caller, so a later response cannot touch a
  buffer a consumer is using. All three consumers (GetData, WM_RENDERFORMAT,
  DELAYED_RENDERING) and the WM_CLIPBOARDUPDATE cleanup use the returned/taken
  handle instead of the shared slot.
- The response handler drops any response arriving while formatDataRespExpected
  is clear (late/duplicate/unsolicited), consumes the flag on the first
  response, and no longer dereferences a NULL clipboard in the SetEvent path.

Pre-existing issue, not introduced by this branch; generalizes the
file-contents hardening to the format-data path.

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

* remove the dedicated wf-cliprdr CI workflow

Drop .github/workflows/wf-cliprdr-ci.yml on this branch as requested.

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

* remove wf-cliprdr invariant tests

Drop tests/test_invariant_wf_cliprdr.c on this branch as requested.

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

* refactor and simplify, remove mutex which is dangeours

* fix copilot false report

* fix review

* Potential fix for pull request finding

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:54:22 +08:00
rustdesk 6c578292e8 bump to 1.4.9 2026-07-06 18:00:39 +08:00
fufesou 28930c0463 fix: non-E2EE show dialog (#15514)
* fix: non-E2EE show dialog

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

* fix: build web, bridge

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

* fix: direct IP access, do not snow non-E2EE dialog

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

* fix: non E2EE dialog, update contents

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

* fix: non-E2EE, show dialog, port forward

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

* fix: non-E2EE dialog, port forward, ignore direct IP access

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

* fix: non-E2EE is_direct_ip_access()

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

* Simple refactor

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

* fix: non-E2EE dialog, port forward, close socket on disconnect

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

* fix: non-E2EE dialog, incorrect reuse of Data::Close

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-06 17:05:11 +08:00
fufesou 37141afece refact: remove feature cli (#15524)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-06 16:17:32 +08:00
fufesou 493b14ba78 Fix/session scope permission audit (#15469)
* fix: enforce session-scoped permissions

Restrict non-remote sessions to their allowed message types, filter
out-of-scope login options, and audit rejected or filtered messages.
Hide screenshot controls outside default remote sessions.

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

* fix: typo

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

* fix: prevent privacy mode in view-camera sessions

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

* fix: switch display, check non-view-camera

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

* fix: avoid sending unsupported messages

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

* fix: session scope, add option to control close/alarm

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

* Fix: scoped session handling for view-camera compatibility

  - Skip view-camera auto-login and display-management side effects
  - Allow harmless render broadcasts without affecting non-video sessions
  - Keep legacy view-camera management messages compatible as no-ops
  - Preserve stricter scope violations for non-video session types

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

* update libs/hbb_common

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

* fix: ignore repeated login request

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

* fix: view camera, support "Take screenshot"

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

* fix: session scoped messages, check update options

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

* fix: session scope, check portforward before conn type voolations

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

* fix: scoped messages, reduce changes.

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

* fix: session scope, comments

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

* fix: keep scoped sessions compatible with render broadcasts

Allow legacy render-broadcast no-op messages for file transfer and terminal
sessions while keeping port forward and mixed options scoped. Also avoid sending
new render updates to non-video Flutter sessions.

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

* fix: scope screenshot requests by video source

Key screenshot requests by video source and display index so camera and
monitor sessions cannot consume each other's requests. Deduplicate the Flutter
render-target predicate while keeping render updates limited to video sessions.

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

* fix: Harden scoped session message handling

Filter option updates by authenticated connection type,
keep legacy no-op messages compatible, and avoid noisy repeated
scope violation alarms.

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

* fix: session scope, comments

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

* fix: Send close reason for scoped session violations

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

* fix: Enforce scoped session message filtering

  - filter out-of-scope messages for limited session types
  - scope option updates by authenticated connection type
  - keep render-broadcast no-op compatibility for non-video scoped sessions
  - restore view-camera screenshot handling
  - improve session scope violation audit labels
  - avoid cloning option messages on the remote hot path

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

* Fix scoped session clipboard broadcast compatibility

Treat text clipboard broadcasts as no-op compatibility messages for FileTransfer and Terminal sessions, matching existing
handler behavior and preventing optional scope-violation close from disconnecting those sessions. Keep ViewCamera and
PortForward clipboard messages subject to normal scope enforcement.

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

* fix: log warn

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

* fix: restrict Flutter clipboard sync to default sessions

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

* fix: session scope, comments and tests

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

* fix: session scope, reset sessions in login handle

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

* fix: session scope, view camera, allow clipboard noop

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-05 23:28:32 +08:00
StealUrKill cf1de4de62 Feature: Restore the last viewed monitor on auto reconnect (#15441)
* Feature: Restore the last viewed monitor on auto reconnect

Remembers the users last manually selected remote monitor and returns to it after an auto reconnect.

In memory, reconnect only, and bounds checked against the current display count.

It is skipped in "use all my displays" mode.

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

* Address review on reconnect monitor restore

Avoid a crash if the session closes during a reconnect.

Don't overwrite the remembered monitor on auto restore.

Defer the switch until the view is ready so a monitor with a different size renders correctly.

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

* Guard all-displays reconnect restore against empty display list

* Harden reconnect monitor restore against races and multi-UI sessions

Cancel a queued restore when the user manually selects a monitor, so a
newer choice is not overridden by a stale pending restore.

Compare the remembered monitor against the reconnect event's display
instead of the stale _pi.currentDisplay, which is intentionally left
unchanged when the peer has multiple sessions.

Add a frame-independent fallback so a multi-UI tab that never receives
the first-image event (its display is filtered to the owning tab) still
restores the remembered monitor.

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

* Harden reconnect monitor restore: fallback timer, lifecycle, cursor

Follow-up hardening on the auto-reconnect monitor restore:

- Cancel the fallback timer synchronously once this tab owns the restore,
  so it can no longer fire while onEvent2UIRgba is awaiting canvas setup
  and switch displays before the canvas is ready (the offset the deferred
  restore exists to avoid). The multi-UI no-frame fallback stays intact.
- Apply the restore in a finally so a throwing canvas init still runs it
  instead of stranding a queued restore with the timer already cancelled.
- Cancel the fallback timer on a manual monitor switch, so a newer user
  selection supersedes a queued restore instead of racing it.
- Restore with updateCursorPos: false, matching other programmatic
  display switches so an auto-restore does not reposition the cursor.

---------

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
2026-07-05 22:32:47 +08:00
fufesou 9fdb8410d3 fix: parse exit code of flutter web (#15501)
* fix: parse exit code of flutter web

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

* fix: exit-code, debug print

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-03 12:49:32 +08:00
alonginwind a2b79462ab fix: auto-close terminal tab/window when shell exits (#15448) 2026-07-02 16:43:27 +08:00
fufesou dce221be5a fix(clipboard): make CLIPRDR format-map growth checked (#15493)
* fix(clipboard): make CLIPRDR format-map growth checked

The Windows CLIPRDR format-list handler relies on map_ensure_capacity()
while processing peer-provided formats. The previous helper only attempted
growth: if realloc() failed, it returned silently and the caller continued
processing. A later iteration could then index past the allocated
format_mappings array.

Make format-map growth a checked operation. The handler now validates the
peer-provided format count, ensures the mapping array is large enough before
writing entries, and aborts processing if growth fails. Newly allocated slots
are zeroed so existing cleanup can safely run after partial processing.

Also bound remote format names before measuring/converting them. The chosen
limits follow Windows clipboard/atom constraints:
  - registered clipboard format IDs use 0xC000..0xFFFF
  - string atom names are limited to 255 bytes

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

* fix(clipboard): reject invalid remote format-list entries

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-02 16:18:07 +08:00
rustdesk 9d1ab3fba3 fix the AOM tile-control argument type 2026-07-01 11:47:08 +08:00
rustdesk b3bd18845d update hbb_common 2026-06-30 11:29:56 +08:00
rustdesk 435f6ec61d update copyright 2026-06-30 11:02:28 +08:00
21pages 0497814004 Add authentication details to connection audit (#15456)
* Add authentication details to connection audit

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

* rename normalize_conn_audit_primary_auth to normalize_conn_audit_auth_fields

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

* Merge permanent password audit methods

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

* Simplify connection audit auth methods

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-29 16:04:24 +08:00
劉清揚 4b1ef9e20d fix(android): sync input service state with Flutter (#15419)
Signed-off-by: liuqiang <2465199797@qq.com>
2026-06-29 15:14:34 +08:00
twprh 10d5250d23 Update flutter-build.yml (#15454) 2026-06-28 17:18:41 +08:00
Maison da Silva 2ee580d49d Update translation for outdated installation message (#15427)
Update translation for outdated installation message
2026-06-28 12:11:03 +08:00
fufesou 4a54029cac fix(update): msi, norestart (#15440)
* fix(update): msi, norestart

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

* fix(update): escape path

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-27 16:45:27 +08:00
fufesou 001848bf2f fix(fuse): umount (#15426)
* fix(clipboard): clean up stale Linux FUSE mounts

Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached
context as valid when the mount has already gone away.

This fixes the desktop file manager copy failure that shows dialogs such as
"Error while copying a" and "There was an error copying the file into xxx".

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

* fix(clipboard): fuse, reduce dups

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

* fix: clear Linux file clipboard before unmounting FUSE

Ensure Linux client teardown clears RustDesk file clipboard URLs while
the FUSE context is still available. Also prefer fusermount before
umount to avoid noisy unprivileged teardown attempts.

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

* fix(clipboard): return and log errors

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-26 16:17:44 +08:00
21pages 989bf80fe8 Support controller user attribution in audit logs (#15407)
* Support controller user attribution in audit logs

This PR supports associating audit logs with the controller user.

  ## Implementation:
  - Add `ControlledContext { conn_audit_token }` to `PunchHole`, `RequestRelay`, and `FetchLocalAddr`.
  - The server sends a controller-user identity snapshot to the controlled client through rendezvous messages.
  - The controlled client sends the token back to the server when posting the `on_open` conn audit or IP whitelist alarm audit.
  - This lets the server attach the controller user to audit logs.

  ## How the controlled client helps identify the controller user:
  - Conn audit: sends the token to the server in `on_open`; the server creates the audit log and caches the user snapshot.
  - File audit: sends `id` and `conn_id`; the server uses them to find the cached user snapshot.
  - Alarm audit: IP whitelist sends the token directly; other alarm logs send `id` and `conn_id`, and the server uses them to find the cached user
  snapshot.

  ## Compatibility:
  - Supported only for logs created with a new server and a new controlled client.
  - Does not require upgrading the controller client.

  ## Test

  - [x] New/old clients connected to new/old servers, and conn/file/alarm audit logs worked normally.
  - [x] New client connected to new server generated searchable conn/file/alarm audit logs.
  - [x] Punch hole, local addr, and relay paths worked with audit logs and control role on new/old servers.
  - [x] Direct IP connections produced audit logs, but do not support user audit.

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

* rename conn_audit_token to conn_audit_ref

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-26 15:07:27 +08:00
VenusGirl❤ 78b5f47668 Update ko.rs (#15395) 2026-06-26 13:51:08 +08:00
jkh0kr 97e9e44faa Update ko.rs (#15390)
Incorrect translation
2026-06-26 11:19:18 +08:00
RAIT-09 ff226f6d80 fix(clipboard): unix, refresh cached file size/mtime on re-copy (#15392)
* fix(clipboard): unix, refresh cached file size/mtime on re-copy

sync_files() deduped re-copies by path string only, so editing a file
and re-copying it (same path) skipped refreshing the cached size/mtime
and the file-group descriptor; the peer then received the file
truncated to the old cached size (silent corruption for PDF/zip/pptx).
Widen the early-return guard to also compare a top-level (size, mtime)
fingerprint and to always rebuild when a directory is selected. The
Windows wf_cliprdr.c path re-stats per request and is unaffected.

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>

* opt(clipboard): unix, compute file fingerprint once and pass into sync_files

fingerprint() was computed before taking the CLIP_FILES lock and then
recomputed inside ClipFiles::sync_files under the lock. Pass the precomputed
value in so the top-level stat runs once and outside the critical section.
No behavior change.

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>

---------

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>
2026-06-25 09:50:33 +08:00
Daniel Marschall 0cbdb6ffb3 Fix tray icon click (regression due to breaking change in tray-icon 0.17) (#15413) 2026-06-25 09:43:04 +08:00
fufesou b8117c5c34 fix(fuse): fuse path broken, since ipc path changed (#15406)
* fix(fuse): fuse path broken, since ipc path changed

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

* fix(fuse): init, handle error

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

* fix(fuse): unmount attempt on newly created directory failed

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-24 16:52:27 +08:00
Maison da Silva a69614d464 Update translation for 'Control Actions' in ptbr.rs (#15386) 2026-06-24 12:26:36 +08:00
fufesou 58ee593e26 fix(custom-client): show options, incoming-only (#15394)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-24 00:26:09 +08:00
fufesou 09bc9056c9 fix(update): win aarch64 (#15389)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-23 17:59:29 +08:00
fufesou 0c6df924d1 refact: file transfer, do this for all conflicts(tasks) (#15385)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-23 11:23:42 +08:00
dependabot[bot] 456817b4f4 Git submodule: bump libs/hbb_common from e50ac3c to 387603f (#15384)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `e50ac3c` to `387603f`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](https://github.com/rustdesk/hbb_common/compare/e50ac3cd4897fa6c6ed545189adb2170c34df636...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-23 10:28:08 +08:00
rustdesk 16570ee34f fix https://github.com/rustdesk/rustdesk/discussions/15355 2026-06-22 22:57:08 +08:00
just-some-tall-bloke 2b40c61d8e Fix spelling and grammar errors in comments (#15370)
- dbus.rs: fix grammar (add 'between', pluralize 'processes')
- win_impl.rs: fix typo 'hight' -> 'high', idiom 'such called' -> 'so-called'
- startwm.sh: fix typo 'loging' -> 'logging'
- lib.rs: fix copy-paste error in doc comments for scroll buttons
- message.proto: fix typo 'Clipobard' -> 'Clipboard'
2026-06-22 15:26:51 +08:00
RustDesk dcc64cdeae cjk (#15379) 2026-06-22 14:47:12 +08:00
136 changed files with 3885 additions and 982 deletions
+2 -1
View File
@@ -44,7 +44,7 @@ env:
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" 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 ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.8" VERSION: "1.4.9"
NDK_VERSION: "r28c" NDK_VERSION: "r28c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -1984,6 +1984,7 @@ jobs:
sudo apt-get install -y libarchive-tools libfuse2 sudo apt-get install -y libarchive-tools libfuse2
# set-up appimage-builder # set-up appimage-builder
# https://github.com/AppImage/AppImageKit/issues/1395 # https://github.com/AppImage/AppImageKit/issues/1395
sudo pip3 install "setuptools_scm<10"
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
# run appimage-builder # run appimage-builder
pushd appimage pushd appimage
+1 -1
View File
@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly" TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.8" VERSION: "1.4.9"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
-85
View File
@@ -1,85 +0,0 @@
name: wf-cliprdr CI
on:
workflow_dispatch:
pull_request:
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
push:
branches:
- master
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: wf_cliprdr invariant test
runs-on: windows-2022
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Setup vcpkg with GitHub Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
doNotCache: false
- name: Install vcpkg dependency
shell: pwsh
run: |
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
- name: Build test
shell: pwsh
run: |
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
New-Item -ItemType Directory -Force $testRoot | Out-Null
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
$cmakeLists = @(
'cmake_minimum_required(VERSION 3.20)'
'project(test_invariant_wf_cliprdr C)'
''
'set(CMAKE_C_STANDARD 11)'
'set(CMAKE_C_STANDARD_REQUIRED ON)'
'set(CMAKE_C_EXTENSIONS OFF)'
''
'find_package(check CONFIG REQUIRED)'
''
'add_executable(test_invariant_wf_cliprdr'
' "TEST_SOURCE"'
')'
''
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
')'
) -join [Environment]::NewLine
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build (Join-Path $testRoot 'out') --config Release
- name: Run test
shell: pwsh
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe
+6
View File
@@ -84,3 +84,9 @@ Then translate that source into the file's target language (infer the language f
* Preserve placeholders (`{}`) and escape sequences (`\n`, `\"`) exactly as in the source. * Preserve placeholders (`{}`) and escape sequences (`\n`, `\"`) exactly as in the source.
* Do not translate brand or technical tokens: `RustDesk`, `Socks5`, `TLS`, `UAC`, `Wayland`, `X11`, `TCP`, `UDP`, `2FA`, `RDP`, `D3D`, etc. * Do not translate brand or technical tokens: `RustDesk`, `Socks5`, `TLS`, `UAC`, `Wayland`, `X11`, `TCP`, `UDP`, `2FA`, `RDP`, `D3D`, etc.
* Copy URL values (e.g. `doc_*` keys) verbatim from `en.rs`. * Copy URL values (e.g. `doc_*` keys) verbatim from `en.rs`.
### Adding new keys (feature work)
* New English-text keys use sentence case, not Title Case: `Use ID whitelisting`, **not** `Use ID Whitelisting`. Acronyms (ID, IP, 2FA…) stay uppercase. Legacy Title-Case keys (e.g. `Use IP Whitelisting`) stay as-is — do not rename them.
* Since the key itself is the English display text, a sentence-case key usually needs **no** `en.rs` entry; add one only when the display text must differ from the key (e.g. `*_tip` keys).
* Append each new key to `template.rs` (with `""`) and to every `src/lang/*.rs` file (translated, or `""` if unsure), at the end of the list.
Generated
+3 -26
View File
@@ -6588,7 +6588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8" checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"rpassword 2.1.0", "rpassword",
"tempfile", "tempfile",
"termios 0.3.3", "termios 0.3.3",
"winapi 0.3.9", "winapi 0.3.9",
@@ -7152,17 +7152,6 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
] ]
[[package]]
name = "rpassword"
version = "7.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
dependencies = [
"libc",
"rtoolbox",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "rtcp" name = "rtcp"
version = "0.14.0" version = "0.14.0"
@@ -7174,16 +7163,6 @@ dependencies = [
"webrtc-util", "webrtc-util",
] ]
[[package]]
name = "rtoolbox"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "rtp" name = "rtp"
version = "0.14.0" version = "0.14.0"
@@ -7270,7 +7249,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.4.8" version = "1.4.9"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
@@ -7283,7 +7262,6 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"chrono", "chrono",
"cidr-utils", "cidr-utils",
"clap 4.5.53",
"clipboard", "clipboard",
"clipboard-master", "clipboard-master",
"cocoa 0.24.1", "cocoa 0.24.1",
@@ -7341,7 +7319,6 @@ dependencies = [
"repng", "repng",
"reqwest", "reqwest",
"ringbuf", "ringbuf",
"rpassword 7.3.1",
"rubato", "rubato",
"runas", "runas",
"rust-pulsectl", "rust-pulsectl",
@@ -7385,7 +7362,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.4.8" version = "1.4.9"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",
+2 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.4.8" version = "1.4.9"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -22,7 +22,6 @@ path = "src/service.rs"
[features] [features]
inline = [] inline = []
cli = []
use_samplerate = ["samplerate"] use_samplerate = ["samplerate"]
use_rubato = ["rubato"] use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
@@ -62,8 +61,6 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol
rubato = { version = "0.12", optional = true } rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true } samplerate = { version = "0.2", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
clap = "4.2"
rpassword = "7.2"
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14" default-net = "0.14"
@@ -213,7 +210,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
libxdo-sys = { path = "libs/libxdo-sys-stub" } libxdo-sys = { path = "libs/libxdo-sys-stub" }
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk" ProductName = "RustDesk"
FileDescription = "RustDesk Remote Desktop" FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe" OriginalFilename = "rustdesk.exe"
+1 -1
View File
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.4.8 version: 1.4.9
exec: usr/share/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
+1 -1
View File
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.4.8 version: 1.4.9
exec: usr/share/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -8,6 +8,7 @@ package com.carriez.flutter_hbb
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path import android.graphics.Path
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -68,6 +69,16 @@ class InputService : AccessibilityService() {
get() = ctx != null get() = ctx != null
} }
private fun notifyInputState() {
val inputState = isOpen.toString()
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to inputState)
)
}
}
private val logTag = "input service" private val logTag = "input service"
private var leftIsDown = false private var leftIsDown = false
private var touchPath = Path() private var touchPath = Path()
@@ -716,6 +727,7 @@ class InputService : AccessibilityService() {
override fun onServiceConnected() { override fun onServiceConnected() {
super.onServiceConnected() super.onServiceConnected()
ctx = this ctx = this
notifyInputState()
val info = AccessibilityServiceInfo() val info = AccessibilityServiceInfo()
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
@@ -734,8 +746,16 @@ class InputService : AccessibilityService() {
override fun onDestroy() { override fun onDestroy() {
ctx = null ctx = null
// Keep this fallback even though onUnbind usually notifies first.
notifyInputState()
super.onDestroy() super.onDestroy()
} }
override fun onUnbind(intent: Intent?): Boolean {
ctx = null
notifyInputState()
return super.onUnbind(intent)
}
override fun onInterrupt() {} override fun onInterrupt() {}
} }
@@ -200,12 +200,13 @@ class MainActivity : FlutterActivity() {
"stop_input" -> { "stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf() InputService.ctx?.disableSelf()
} else {
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
} }
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true) result.success(true)
} }
"cancel_notification" -> { "cancel_notification" -> {
+55 -56
View File
@@ -6,83 +6,82 @@ ANDROID_ABI=$1
# Build RustDesk dependencies for Android using vcpkg.json # Build RustDesk dependencies for Android using vcpkg.json
# Required: # Required:
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 1. set VCPKG_ROOT / ANDROID_NDK_HOME path environment variables
# 2. vcpkg initialized # 2. vcpkg initialized
# 3. ndk, version: r25c or newer # 3. ndk, version: r25c or newer
if [ -z "$ANDROID_NDK_HOME" ]; then if [ -z "${ANDROID_NDK_HOME}" ]; then
echo "Failed! Please set ANDROID_NDK_HOME" echo "ERROR: Please set ANDROID_NDK_HOME environment variable" 1>&2
exit 1 exit 1
fi fi
if [ -z "$VCPKG_ROOT" ]; then if [ -z "${VCPKG_ROOT}" ]; then
echo "Failed! Please set VCPKG_ROOT" echo "ERROR: Please set VCPKG_ROOT environment variable" 1>&2
exit 1 exit 1
fi fi
API_LEVEL="21" case "${ANDROID_ABI}" in
arm64-v8a)
VCPKG_TARGET=arm64-android
;;
armeabi-v7a)
VCPKG_TARGET=arm-neon-android
;;
x86_64)
VCPKG_TARGET=x64-android
;;
x86)
VCPKG_TARGET=x86-android
;;
*)
echo "Usage: build_android_deps.sh <arm64-v8a|armeabi-v7a|x86_64|x86>" 1>&2
exit 1
;;
esac
# Get directory of this script # Get directory of this script
SCRIPTDIR="$(readlink -f "$0")" SCRIPTDIR="$(readlink -f "$0")"
SCRIPTDIR="$(dirname "$SCRIPTDIR")" SCRIPTDIR="$(dirname "${SCRIPTDIR}")"
# Check if vcpkg.json is one level up - in root directory of RD # Check if vcpkg.json is one level up - in root directory of RD
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then if [ ! -f "${SCRIPTDIR}/../vcpkg.json" ]; then
echo "Failed! Please check where vcpkg.json is!" echo "ERROR: Can not find vcpkg.json in RustDesk top-level directory" 1>&2
exit 1 exit 1
fi fi
# NDK llvm toolchain echo "INFO: Building and install vcpkg dependencies for Android ${ANDROID_ABI} ..."
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/` pushd "${SCRIPTDIR}/.."
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
function build { "${VCPKG_ROOT}/vcpkg" install \
ANDROID_ABI=$1 --triplet "${VCPKG_TARGET}" \
--x-install-root="${VCPKG_ROOT}/installed"
case "$ANDROID_ABI" in popd
arm64-v8a)
ABI=aarch64-linux-android$API_LEVEL
VCPKG_TARGET=arm64-android
;;
armeabi-v7a)
ABI=armv7a-linux-androideabi$API_LEVEL
VCPKG_TARGET=arm-neon-android
;;
x86_64)
ABI=x86_64-linux-android$API_LEVEL
VCPKG_TARGET=x64-android
;;
x86)
ABI=i686-linux-android$API_LEVEL
VCPKG_TARGET=x86-android
;;
*)
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
return 1
esac
echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies" echo "INFO: Completed building vcpkg dependencies for Android ${ANDROID_ABI}"
pushd "$SCRIPTDIR/.."
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
popd
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then if [ "${ANDROID_ABI}" = 'armeabi-v7a' ]; then
echo "*** [Start] Move arm-neon-android to arm-android" # Symlink arm-neon-android to arm-android because cargo-ndk does not
# understand NEON suffix.
mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android" if [ -d "${VCPKG_ROOT}/installed/arm-neon-android" ]; then
echo 'INFO: Symlinking arm-neon-android to arm-android'
echo "*** [Finished] Move arm-neon-android to arm-android" ln -sf \
fi "${VCPKG_ROOT}/installed/arm-neon-android" \
} "${VCPKG_ROOT}/installed/arm-android"
if [ ! -z "$ANDROID_ABI" ]; then echo 'INFO: Symlinked arm-neon-android to arm-android'
build "$ANDROID_ABI" else
else cat 0<<.a
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2 ERROR: 'vcpkg install' seem to complete successfully but
exit 1 directory '${VCPKG_ROOT}/installed/arm-neon-android' is missing!
.a
exit 1
fi
fi fi
+53 -1
View File
@@ -1185,6 +1185,48 @@ void msgBox(SessionID sessionId, String type, String title, String text,
VoidCallback? onSubmit, VoidCallback? onSubmit,
int? submitTimeout}) { int? submitTimeout}) {
dialogManager.dismissAll(); dialogManager.dismissAll();
if (type.contains('insecure-connection')) {
Future<void> closeSession() async {
await bind.sessionSetCommon(
sessionId: sessionId,
key: 'continue-insecure-connection',
value: 'N',
);
dialogManager.dismissAll();
closeConnection();
}
void continueSession() {
unawaited(
bind.sessionSetCommon(
sessionId: sessionId,
key: 'continue-insecure-connection',
value: 'Y',
),
);
dialogManager.dismissAll();
}
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(child: msgboxContent(type, title, text)),
actions: [
dialogButton(
'Continue',
onPressed: continueSession,
isOutline: true,
),
dialogButton('Disconnect', onPressed: closeSession),
],
onSubmit: closeSession,
onCancel: closeSession,
),
tag: '$sessionId-$type-$title-$text-$link',
);
return;
}
List<Widget> buttons = []; List<Widget> buttons = [];
bool hasOk = false; bool hasOk = false;
submit() { submit() {
@@ -3350,7 +3392,12 @@ Future<List<Rect>> getScreenRectList() async {
} }
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi, openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
{bool updateCursorPos = true}) { {bool updateCursorPos = true, bool recordSelection = true}) {
if (recordSelection) {
ffi.ffiModel.lastUserDisplay = i;
ffi.ffiModel.cancelPendingRestoreTimer();
ffi.ffiModel.pendingMonitorRestore = null;
}
final displays = i == kAllDisplayValue final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index) ? List.generate(pi.displays.length, (index) => index)
: [i]; : [i];
@@ -3957,6 +4004,11 @@ bool whitelistNotEmpty() {
return v != '' && v != ','; return v != '' && v != ',';
} }
bool idWhitelistNotEmpty() {
final v = bind.mainGetOptionSync(key: kOptionIdWhitelist);
return v != '' && v != ',';
}
// `setMovable()` is only supported on macOS. // `setMovable()` is only supported on macOS.
// //
// On macOS, the window can be dragged by the tab bar by default. // On macOS, the window can be dragged by the tab bar by default.
+106
View File
@@ -205,6 +205,10 @@ void changeWhiteList({Function()? callback}) async {
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Text(translate("whitelist_cidr_tip")),
const SizedBox(
height: 8.0,
),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -282,6 +286,108 @@ void changeWhiteList({Function()? callback}) async {
}); });
} }
void changeIdWhiteList({Function()? callback}) async {
final curIdWhiteList = await bind.mainGetOption(key: kOptionIdWhitelist);
var newIdWhiteListField = curIdWhiteList == defaultOptionWhitelist
? ''
: curIdWhiteList.split(',').join('\n');
var controller = TextEditingController(text: newIdWhiteListField);
var msg = "";
var isInProgress = false;
final isOptFixed = isOptionFixed(kOptionIdWhitelist);
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("ID whitelisting")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Text(translate("id_whitelist_wildcard_tip")),
const SizedBox(
height: 8.0,
),
Text(translate("id_whitelist_caveat_tip")),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
enabled: !isOptFixed,
autofocus: true)
.workaroundFreezeLinuxMint(),
),
],
),
const SizedBox(
height: 4.0,
),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
if (!isOptFixed)
dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(
key: kOptionIdWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
}, isOutline: true),
if (!isOptFixed)
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newIdWhiteListField = controller.text.trim();
var newIdWhiteList = "";
if (newIdWhiteListField.isEmpty) {
// pass
} else {
final ids =
newIdWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test id, wildcards '*' and '?' are allowed;
// '@' '.' ':' occur in cross-server IDs like "123456789@server:port"
final idMatch = RegExp(r"^[a-zA-Z0-9\_\-\*\?\@\.\:]+$");
for (final id in ids) {
if (!idMatch.hasMatch(id)) {
msg = "${translate("Invalid ID")} $id";
setState(() {
isInProgress = false;
});
return;
}
}
newIdWhiteList = ids.join(',');
}
if (newIdWhiteList.trim().isEmpty) {
newIdWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionIdWhitelist, value: newIdWhiteList);
callback?.call();
close();
},
),
],
onCancel: close,
);
});
}
Future<String> changeDirectAccessPort( Future<String> changeDirectAccessPort(
String currentIP, String currentPort) async { String currentIP, String currentPort) async {
final controller = TextEditingController(text: currentPort); final controller = TextEditingController(text: currentPort);
+1 -1
View File
@@ -606,7 +606,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
// to-do: // to-do:
// 1. Web desktop // 1. Web desktop
// 2. Mobile, copy the image to the clipboard // 2. Mobile, copy the image to the clipboard
if (isDesktop) { if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
final isScreenshotSupported = bind.sessionGetCommonSync( final isScreenshotSupported = bind.sessionGetCommonSync(
sessionId: sessionId, key: 'is_screenshot_supported', param: ''); sessionId: sessionId, key: 'is_screenshot_supported', param: '');
if ('true' == isScreenshotSupported) { if ('true' == isScreenshotSupported) {
+1
View File
@@ -95,6 +95,7 @@ const String kOptionForceAlwaysRelay = "force-always-relay";
const String kOptionViewOnly = "view_only"; const String kOptionViewOnly = "view_only";
const String kOptionEnableLanDiscovery = "enable-lan-discovery"; const String kOptionEnableLanDiscovery = "enable-lan-discovery";
const String kOptionWhitelist = "whitelist"; const String kOptionWhitelist = "whitelist";
const String kOptionIdWhitelist = "id-whitelist";
const String kOptionEnableAbr = "enable-abr"; const String kOptionEnableAbr = "enable-abr";
const String kOptionEnableRecordSession = "enable-record-session"; const String kOptionEnableRecordSession = "enable-record-session";
const String kOptionDirectServer = "direct-server"; const String kOptionDirectServer = "direct-server";
@@ -483,13 +483,15 @@ class _GeneralState extends State<_General> {
} }
Widget other() { Widget other() {
final incomingOnly = bind.isIncomingOnly();
final outgoingOnly = bind.isOutgoingOnly();
final showAutoUpdate = isWindows && bind.mainIsInstalled(); final showAutoUpdate = isWindows && bind.mainIsInstalled();
final children = <Widget>[ final children = <Widget>[
if (!isWeb && !bind.isIncomingOnly()) if (!isWeb && !incomingOnly)
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs, kOptionEnableConfirmClosingTabs,
isServer: false), isServer: false),
if (!bind.isIncomingOnly()) if (!incomingOnly)
_OptionCheckBox( _OptionCheckBox(
context, context,
'allow-remote-toolbar-docking-any-edge', 'allow-remote-toolbar-docking-any-edge',
@@ -499,9 +501,10 @@ class _GeneralState extends State<_General> {
reloadAllWindows(); reloadAllWindows();
}, },
), ),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), if (!isWeb && !outgoingOnly)
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(), if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[ if (!isWeb && !incomingOnly) ...[
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@@ -540,40 +543,40 @@ class _GeneralState extends State<_General> {
isServer: false, isServer: false,
), ),
), ),
if (!isWeb && !bind.isCustomClient()) ],
_OptionCheckBox( if (!isWeb && !bind.isCustomClient())
context, _OptionCheckBox(
'Check for software update on startup', context,
kOptionEnableCheckUpdate, 'Check for software update on startup',
isServer: false, kOptionEnableCheckUpdate,
), isServer: false,
if (showAutoUpdate) ),
_OptionCheckBox( if (showAutoUpdate)
context, _OptionCheckBox(
'Auto update', context,
kOptionAllowAutoUpdate, 'Auto update',
isServer: true, kOptionAllowAutoUpdate,
), isServer: true,
if (isWindows && !bind.isOutgoingOnly()) ),
_OptionCheckBox( if (isWindows && !outgoingOnly)
context, _OptionCheckBox(
'Capture screen using DirectX', context,
kOptionDirectxCapture, 'Capture screen using DirectX',
), kOptionDirectxCapture,
if (!bind.isIncomingOnly()) ...[ ),
_OptionCheckBox( if (!isWeb && !incomingOnly) ...[
context, _OptionCheckBox(
'Enable UDP hole punching', context,
kOptionEnableUdpPunch, 'Enable UDP hole punching',
isServer: false, kOptionEnableUdpPunch,
), isServer: false,
_OptionCheckBox( ),
context, _OptionCheckBox(
'Enable IPv6 P2P connection', context,
kOptionEnableIpv6Punch, 'Enable IPv6 P2P connection',
isServer: false, kOptionEnableIpv6Punch,
), isServer: false,
], ),
], ],
]; ];
@@ -1294,6 +1297,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
reverse: true, enabled: enabled), reverse: true, enabled: enabled),
...directIp(context), ...directIp(context),
whitelist(), whitelist(),
idWhitelist(),
...autoDisconnect(context), ...autoDisconnect(context),
_OptionCheckBox(context, 'keep-awake-during-incoming-sessions-label', _OptionCheckBox(context, 'keep-awake-during-incoming-sessions-label',
kOptionKeepAwakeDuringIncomingSessions, kOptionKeepAwakeDuringIncomingSessions,
@@ -1451,6 +1455,52 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return tmpWrapper(); return tmpWrapper();
} }
Widget idWhitelist() {
bool enabled = !locked;
RxBool hasIdWhitelist = idWhitelistNotEmpty().obs;
update() async {
hasIdWhitelist.value = idWhitelistNotEmpty();
}
onChanged(bool? checked) async {
changeIdWhiteList(callback: update);
}
final isOptFixed = isOptionFixed(kOptionIdWhitelist);
return GestureDetector(
child: Tooltip(
message: translate('id_whitelist_tip'),
child: Obx(() => Row(
children: [
Checkbox(
value: hasIdWhitelist.value,
onChanged: enabled && !isOptFixed ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !hasIdWhitelist.value,
child: MouseRegion(
child: const Icon(Icons.warning_amber_rounded,
color: Color.fromARGB(255, 255, 204, 0))
.marginOnly(right: 5),
cursor: SystemMouseCursors.click,
),
),
Expanded(
child: Text(
translate('Use ID whitelisting'),
style: TextStyle(color: disabledTextColor(context, enabled)),
))
],
)),
),
onTap: enabled
? () {
onChanged(!hasIdWhitelist.value);
}
: null,
).marginOnly(left: _kCheckBoxLeftMargin);
}
Widget hide_cm(bool enabled) { Widget hide_cm(bool enabled) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: gFFI.serverModel, value: gFFI.serverModel,
@@ -2411,17 +2461,20 @@ class _AboutState extends State<_About> {
final version = await bind.mainGetVersion(); final version = await bind.mainGetVersion();
final buildDate = await bind.mainGetBuildDate(); final buildDate = await bind.mainGetBuildDate();
final fingerprint = await bind.mainGetFingerprint(); final fingerprint = await bind.mainGetFingerprint();
final myId = await bind.mainGetMyId();
return { return {
'license': license, 'license': license,
'version': version, 'version': version,
'buildDate': buildDate, 'buildDate': buildDate,
'fingerprint': fingerprint 'fingerprint': fingerprint,
'myId': myId
}; };
}(), hasData: (data) { }(), hasData: (data) {
final license = data['license'].toString(); final license = data['license'].toString();
final version = data['version'].toString(); final version = data['version'].toString();
final buildDate = data['buildDate'].toString(); final buildDate = data['buildDate'].toString();
final fingerprint = data['fingerprint'].toString(); final fingerprint = data['fingerprint'].toString();
final myId = data['myId'].toString();
const linkStyle = TextStyle(decoration: TextDecoration.underline); const linkStyle = TextStyle(decoration: TextDecoration.underline);
final scrollController = ScrollController(); final scrollController = ScrollController();
return SingleChildScrollView( return SingleChildScrollView(
@@ -2443,6 +2496,9 @@ class _AboutState extends State<_About> {
SelectionArea( SelectionArea(
child: Text('${translate('Fingerprint')}: $fingerprint') child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)), .marginSymmetric(vertical: 4.0)),
SelectionArea(
child: Text('${translate('ID')}: $myId')
.marginSymmetric(vertical: 4.0)),
InkWell( InkWell(
onTap: () { onTap: () {
launchUrlString('https://rustdesk.com/privacy.html'); launchUrlString('https://rustdesk.com/privacy.html');
@@ -2471,7 +2527,7 @@ class _AboutState extends State<_About> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
Text( Text(
+2 -2
View File
@@ -495,14 +495,14 @@ class _CmHeaderState extends State<_CmHeader>
if (client.type_() == ClientType.file) if (client.type_() == ClientType.file)
FittedBox( FittedBox(
child: Text( child: Text(
translate("File Transfer"), translate("Transfer file"),
style: TextStyle(color: Colors.white70, fontSize: 12), style: TextStyle(color: Colors.white70, fontSize: 12),
), ),
), ),
if (client.type_() == ClientType.camera) if (client.type_() == ClientType.camera)
FittedBox( FittedBox(
child: Text( child: Text(
translate("View Camera"), translate("View camera"),
style: TextStyle(color: Colors.white70, fontSize: 12), style: TextStyle(color: Colors.white70, fontSize: 12),
), ),
), ),
@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing // Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel); _ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close tab when shell exits
_terminalModel.onClosed = () {
if (mounted) {
widget.tabController.closeBy(widget.tabKey);
}
};
// Initialize terminal connection // Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id); widget.tabController.onSelected?.call(widget.id);
@@ -1167,8 +1167,8 @@ class _MonitorMenu extends StatelessWidget {
tooltip: isMulti tooltip: isMulti
? '' ? ''
: isAllMonitors : isAllMonitors
? 'all monitors' ? 'All monitors'
: '#${i + 1} monitor', : '#{${i + 1}} monitor',
hMargin: isMulti ? null : 6, hMargin: isMulti ? null : 6,
vMargin: isMulti ? null : 12, vMargin: isMulti ? null : 12,
topLevel: false, topLevel: false,
@@ -2852,7 +2852,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin, horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin); vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
button = Tooltip( button = Tooltip(
message: widget.tooltip, message: translate(widget.tooltip),
child: button, child: button,
); );
if (widget.topLevel) { if (widget.topLevel) {
@@ -78,6 +78,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _enableAbr = false; var _enableAbr = false;
var _denyLANDiscovery = false; var _denyLANDiscovery = false;
var _onlyWhiteList = false; var _onlyWhiteList = false;
var _onlyIdWhiteList = false;
var _enableDirectIPAccess = false; var _enableDirectIPAccess = false;
var _enableRecordSession = false; var _enableRecordSession = false;
var _enableHardwareCodec = false; var _enableHardwareCodec = false;
@@ -89,6 +90,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _directAccessPort = ""; var _directAccessPort = "";
var _fingerprint = ""; var _fingerprint = "";
var _buildDate = ""; var _buildDate = "";
var _myId = "";
var _autoDisconnectTimeout = ""; var _autoDisconnectTimeout = "";
var _hideServer = false; var _hideServer = false;
var _hideProxy = false; var _hideProxy = false;
@@ -109,6 +111,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery, _denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery)); bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = whitelistNotEmpty(); _onlyWhiteList = whitelistNotEmpty();
_onlyIdWhiteList = idWhitelistNotEmpty();
_enableDirectIPAccess = option2bool( _enableDirectIPAccess = option2bool(
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer)); kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
_enableRecordSession = option2bool(kOptionEnableRecordSession, _enableRecordSession = option2bool(kOptionEnableRecordSession,
@@ -217,6 +220,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_buildDate = buildDate; _buildDate = buildDate;
} }
final myId = await bind.mainGetMyId();
if (_myId != myId) {
update = true;
_myId = myId;
}
final isUsingPublicServer = await bind.mainIsUsingPublicServer(); final isUsingPublicServer = await bind.mainIsUsingPublicServer();
if (_isUsingPublicServer != isUsingPublicServer) { if (_isUsingPublicServer != isUsingPublicServer) {
update = true; update = true;
@@ -400,6 +409,29 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
changeWhiteList(callback: update); changeWhiteList(callback: update);
}, },
), ),
SettingsTile.switchTile(
title: Row(children: [
Expanded(child: Text(translate('Use ID whitelisting'))),
Offstage(
offstage: !_onlyIdWhiteList,
child: const Icon(Icons.warning_amber_rounded,
color: Color.fromARGB(255, 255, 204, 0)))
.marginOnly(left: 5)
]),
initialValue: _onlyIdWhiteList,
onToggle: (_) async {
update() async {
final onlyIdWhiteList = idWhitelistNotEmpty();
if (onlyIdWhiteList != _onlyIdWhiteList) {
setState(() {
_onlyIdWhiteList = onlyIdWhiteList;
});
}
}
changeIdWhiteList(callback: update);
},
),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('Adaptive bitrate')), title: Text(translate('Adaptive bitrate')),
initialValue: _enableAbr, initialValue: _enableAbr,
@@ -982,6 +1014,14 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
child: Text(_fingerprint), child: Text(_fingerprint),
), ),
leading: Icon(Icons.fingerprint)), leading: Icon(Icons.fingerprint)),
SettingsTile(
onPressed: (context) => onCopyFingerprint(_myId),
title: Text(translate("ID")),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(_myId),
),
leading: Icon(Icons.perm_identity)),
SettingsTile( SettingsTile(
title: Text(translate("Privacy Statement")), title: Text(translate("Privacy Statement")),
onPressed: (context) => onPressed: (context) =>
@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing // Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel); _ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close connection when shell exits
_terminalModel.onClosed = () {
if (mounted) {
closeConnection(id: widget.id);
}
};
// Web desktop users have full hardware keyboard access, so the on-screen // Web desktop users have full hardware keyboard access, so the on-screen
// terminal extra keys bar is unnecessary and disabled. // terminal extra keys bar is unnecessary and disabled.
_showTerminalExtraKeys = !isWebDesktop && _showTerminalExtraKeys = !isWebDesktop &&
+164 -13
View File
@@ -142,12 +142,22 @@ class FileModel {
} }
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async { Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore stale override confirm event: $evt");
return;
}
evtLoop.pushEvent( evtLoop.pushEvent(
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
} }
Future<void> overrideFileConfirm(Map<String, dynamic> evt, Future<void> overrideFileConfirm(Map<String, dynamic> evt,
{bool? overrideConfirm, bool skip = false}) async { {bool? overrideConfirm, bool skip = false}) async {
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm for inactive job: $evt");
return;
}
// If `skip == true`, it means to skip this file without showing dialog. // If `skip == true`, it means to skip this file without showing dialog.
// Because `resp` may be null after the user operation or the last remembered operation, // Because `resp` may be null after the user operation or the last remembered operation,
// and we should distinguish them. // and we should distinguish them.
@@ -156,15 +166,12 @@ class FileModel {
? await showFileConfirmDialog(translate("Overwrite"), ? await showFileConfirmDialog(translate("Overwrite"),
"${evt['read_path']}", true, evt['is_identical'] == "true") "${evt['read_path']}", true, evt['is_identical'] == "true")
: null); : null);
final id = int.tryParse(evt['id']) ?? 0; if (!jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm result for inactive job: $evt");
return;
}
if (false == resp) { if (false == resp) {
final jobIndex = jobController.getJob(id); await jobController.cancelTransferConflictBatch(id);
if (jobIndex != -1) {
await jobController.cancelJob(id);
final job = jobController.jobTable[jobIndex];
job.state = JobState.done;
jobController.jobTable.refresh();
}
} else { } else {
var need_override = false; var need_override = false;
if (resp == null) { if (resp == null) {
@@ -176,6 +183,7 @@ class FileModel {
} }
// Update the loop config. // Update the loop config.
if (fileConfirmCheckboxRemember) { if (fileConfirmCheckboxRemember) {
jobController.rememberTransferConflictBatch(id, resp);
evtLoop.setSkip(!need_override); evtLoop.setSkip(!need_override);
} }
await bind.sessionSetConfirmOverrideFile( await bind.sessionSetConfirmOverrideFile(
@@ -285,6 +293,8 @@ class FileModel {
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false); final jobID = jobController.addTransferJob(entry, false);
jobController.registerTransferConflictBatch([jobID],
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
webSendLocalFiles( webSendLocalFiles(
handleIndex: handleIndex, handleIndex: handleIndex,
actId: jobID, actId: jobID,
@@ -570,8 +580,15 @@ class FileController {
final toPath = otherSideData.directory.path; final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
final transferJobs = <(Entry, int)>[];
final transferJobIds = <int>[];
for (var from in items.items) { for (var from in items.items) {
final jobID = jobController.addTransferJob(from, isRemoteToLocal); final jobID = jobController.addTransferJob(from, isRemoteToLocal);
transferJobs.add((from, jobID));
transferJobIds.add(jobID);
}
jobController.registerTransferConflictBatch(transferJobIds);
for (final (from, jobID) in transferJobs) {
bind.sessionSendFiles( bind.sessionSendFiles(
sessionId: sessionId, sessionId: sessionId,
actId: jobID, actId: jobID,
@@ -917,6 +934,10 @@ class JobController {
static final JobID jobID = JobID(); static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs; final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>(); final jobResultListener = JobResultListener<Map<String, dynamic>>();
int _nextTransferConflictBatchId = 1;
final Map<int, int> _transferConflictJobToBatch = {};
int? _transferConflictRememberBatchId;
bool? _transferConflictRememberOverrideConfirm;
final GetSessionID getSessionID; final GetSessionID getSessionID;
final GetDialogManager getDialogManager; final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
@@ -929,6 +950,57 @@ class JobController {
return jobTable.indexWhere((element) => element.id == id); return jobTable.indexWhere((element) => element.id == id);
} }
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
final ids = jobIds.toList(growable: false);
if (ids.isEmpty) {
return;
}
batchId ??= _nextTransferConflictBatchId++;
if (batchId >= _nextTransferConflictBatchId) {
_nextTransferConflictBatchId = batchId + 1;
}
for (final jobId in ids) {
_transferConflictJobToBatch[jobId] = batchId;
}
}
int? transferConflictBatchId(int jobId) {
return _transferConflictJobToBatch[jobId];
}
bool hasTransferConflictJob(int jobId) {
return transferConflictBatchId(jobId) != null;
}
bool isTransferConflictRememberBatch(int? batchId) {
return batchId != null && batchId == _transferConflictRememberBatchId;
}
bool? transferConflictRememberOverrideConfirm(int? batchId) {
if (!isTransferConflictRememberBatch(batchId)) {
return null;
}
return _transferConflictRememberOverrideConfirm;
}
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
_transferConflictRememberOverrideConfirm = overrideConfirm;
}
void unregisterTransferConflictJob(int jobId) {
final batchId = _transferConflictJobToBatch.remove(jobId);
if (batchId == null) {
return;
}
if (!_transferConflictJobToBatch.containsValue(batchId)) {
if (_transferConflictRememberBatchId == batchId) {
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
}
}
}
// return jobID // return jobID
int addTransferJob(Entry from, bool isRemoteToLocal) { int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next(); final jobID = JobController.jobID.next();
@@ -1000,7 +1072,10 @@ class JobController {
id = int.parse(evt['id']); id = int.parse(evt['id']);
} catch (_) {} } catch (_) {}
final jobIndex = getJob(id); final jobIndex = getJob(id);
if (jobIndex == -1) return true; if (jobIndex == -1) {
unregisterTransferConflictJob(id);
return true;
}
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.recvJobRes = true; job.recvJobRes = true;
if (job.type == JobType.deleteFile) { if (job.type == JobType.deleteFile) {
@@ -1026,6 +1101,9 @@ class JobController {
job.state = JobState.done; job.state = JobState.done;
} }
jobTable.refresh(); jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(id);
}
if (job.type == JobType.deleteDir) { if (job.type == JobType.deleteDir) {
return job.state == JobState.done; return job.state == JobState.done;
} else { } else {
@@ -1035,9 +1113,15 @@ class JobController {
void jobError(Map<String, dynamic> evt) { void jobError(Map<String, dynamic> evt) {
final err = evt['err'].toString(); final err = evt['err'].toString();
int jobIndex = getJob(int.parse(evt['id'])); final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null) {
debugPrint("Ignore job error with invalid id: $evt");
return;
}
int jobIndex = getJob(id);
if (jobIndex != -1) { if (jobIndex != -1) {
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
if (job.state == JobState.done && job.err == "cancel") return;
job.state = JobState.error; job.state = JobState.error;
job.err = err; job.err = err;
job.recvJobRes = true; job.recvJobRes = true;
@@ -1060,6 +1144,11 @@ class JobController {
} }
} }
jobTable.refresh(); jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(job.id);
}
} else {
unregisterTransferConflictJob(id);
} }
if (err == _kOneWayFileTransferError) { if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
@@ -1096,9 +1185,42 @@ class JobController {
} }
Future<void> cancelJob(int id) async { Future<void> cancelJob(int id) async {
unregisterTransferConflictJob(id);
await bind.sessionCancelJob(sessionId: sessionId, actId: id); await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} }
Future<void> cancelTransferConflictBatch(int jobId) async {
final batchId = _transferConflictJobToBatch[jobId];
final batchJobIds = batchId == null ? [jobId] : <int>[];
if (batchId != null) {
for (final entry in _transferConflictJobToBatch.entries) {
if (entry.value == batchId) {
batchJobIds.add(entry.key);
}
}
for (final id in batchJobIds) {
unregisterTransferConflictJob(id);
}
}
final jobIdsToCancel = batchJobIds.toSet();
for (final job in jobTable) {
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
continue;
}
job.state = JobState.done;
job.err = "cancel";
job.recvJobRes = true;
}
jobTable.refresh();
for (final id in batchJobIds) {
try {
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} catch (e) {
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
}
}
}
Future<void> loadLastJob(Map<String, dynamic> evt) async { Future<void> loadLastJob(Map<String, dynamic> evt) async {
debugPrint("load last job: $evt"); debugPrint("load last job: $evt");
Map<String, dynamic> jobDetail = json.decode(evt['value']); Map<String, dynamic> jobDetail = json.decode(evt['value']);
@@ -1145,7 +1267,7 @@ class JobController {
..state = JobState.paused; ..state = JobState.paused;
jobTable.add(jobProgress); jobTable.add(jobProgress);
} }
registerTransferConflictBatch([currJobId]);
await bind.sessionAddJob( await bind.sessionAddJob(
sessionId: sessionId, sessionId: sessionId,
isRemote: isRemote, isRemote: isRemote,
@@ -1193,6 +1315,9 @@ class JobController {
void clear() { void clear() {
jobTable.clear(); jobTable.clear();
_transferConflictJobToBatch.clear();
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
jobResultListener.clear(); jobResultListener.clear();
} }
} }
@@ -1535,6 +1660,9 @@ class JobProgress {
String display() { String display() {
if (type == JobType.transfer) { if (type == JobType.transfer) {
if (state == JobState.done && err == "cancel") {
return translate("Cancel");
}
if (state == JobState.done && err == "skipped") { if (state == JobState.done && err == "skipped") {
return translate("Skipped"); return translate("Skipped");
} }
@@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
class FileDialogEventLoop class FileDialogEventLoop
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> { extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
int? _batchId;
bool? _overrideConfirm; bool? _overrideConfirm;
bool _skip = false; bool _skip = false;
@override @override
Future<void> onPreConsume( Future<void> onPreConsume(
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async { BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
var event = evt as _FileDialogEvent; final event = evt as _FileDialogEvent;
final model = event.fileModel.target;
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
final batchId = model == null || jobId == null
? null
: model.jobController.transferConflictBatchId(jobId);
final keepRemembered = model != null &&
model.jobController.isTransferConflictRememberBatch(batchId);
// The loop only preloads the remembered batch choice. The model updates it
// after the user answers the current overwrite dialog.
if (_batchId != batchId && !keepRemembered) {
_batchId = batchId;
_overrideConfirm = null;
_skip = false;
} else {
_batchId = batchId;
}
if (keepRemembered) {
_overrideConfirm =
model.jobController.transferConflictRememberOverrideConfirm(batchId);
_skip = _overrideConfirm == null;
}
event.setOverrideConfirm(_overrideConfirm); event.setOverrideConfirm(_overrideConfirm);
event.setSkip(_skip); event.setSkip(_skip);
debugPrint( debugPrint(
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>"); "FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
} }
@override @override
Future<void> onEventsClear() { Future<void> onEventsClear() {
_batchId = null;
_overrideConfirm = null; _overrideConfirm = null;
_skip = false; _skip = false;
return super.onEventsClear(); return super.onEventsClear();
+60 -5
View File
@@ -112,6 +112,9 @@ class CachedPeerData {
class FfiModel with ChangeNotifier { class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData(); CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo(); PeerInfo _pi = PeerInfo();
int? lastUserDisplay;
int? pendingMonitorRestore;
Timer? _pendingRestoreTimer;
Rect? _rect; Rect? _rect;
var _inputBlocked = false; var _inputBlocked = false;
@@ -248,6 +251,8 @@ class FfiModel with ChangeNotifier {
clear() { clear() {
_pi = PeerInfo(); _pi = PeerInfo();
lastUserDisplay = null;
_cancelPendingMonitorRestore();
_secure = null; _secure = null;
_direct = null; _direct = null;
_inputBlocked = false; _inputBlocked = false;
@@ -932,6 +937,7 @@ class FfiModel with ChangeNotifier {
// frame briefly, then shows the Connecting overlay. // frame briefly, then shows the Connecting overlay.
if (_restartReconnectDelayTimer == null) { if (_restartReconnectDelayTimer == null) {
parent.target?.inputModel.setRelativeMouseMode(false); parent.target?.inputModel.setRelativeMouseMode(false);
_cancelPendingMonitorRestore();
bind.sessionReconnect(sessionId: sessionId, forceRelay: false); bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
clearPermissions(); clearPermissions();
// Retry once more after the silent window so restart reconnect attempts // Retry once more after the silent window so restart reconnect attempts
@@ -1084,10 +1090,22 @@ class FfiModel with ChangeNotifier {
} }
} }
void _cancelPendingMonitorRestore() {
_pendingRestoreTimer?.cancel();
_pendingRestoreTimer = null;
pendingMonitorRestore = null;
}
void cancelPendingRestoreTimer() {
_pendingRestoreTimer?.cancel();
_pendingRestoreTimer = null;
}
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId, void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
bool forceRelay) { bool forceRelay) {
// Disable relative mouse mode before reconnecting to ensure cursor is released. // Disable relative mouse mode before reconnecting to ensure cursor is released.
parent.target?.inputModel.setRelativeMouseMode(false); parent.target?.inputModel.setRelativeMouseMode(false);
_cancelPendingMonitorRestore();
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay); bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
clearPermissions(); clearPermissions();
dialogManager.dismissAll(); dialogManager.dismissAll();
@@ -1401,6 +1419,25 @@ class FfiModel with ChangeNotifier {
// now replaced to _updateCurDisplay // now replaced to _updateCurDisplay
updateCurDisplay(sessionId); updateCurDisplay(sessionId);
} }
// After reconnecting, restore the last selected monitor once the canvas is ready.
// Switching earlier can offset the view if the monitor sizes differ.
final last = lastUserDisplay;
pendingMonitorRestore = (!isCache &&
last != null &&
last != currentDisplay &&
bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId) !=
'Y' &&
((last == kAllDisplayValue && _pi.displays.isNotEmpty) ||
(last >= 0 && last < _pi.displays.length)))
? last
: null;
// Fallback if the first image event never reaches this tab (multi-UI).
_pendingRestoreTimer?.cancel();
if (pendingMonitorRestore != null) {
_pendingRestoreTimer = Timer(const Duration(milliseconds: 1500),
() => parent.target?._applyPendingMonitorRestore());
}
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
_reconnects = 1; _reconnects = 1;
_offlineReconnectStartTime = null; _offlineReconnectStartTime = null;
@@ -3911,17 +3948,35 @@ class FFI {
} }
if (ffiModel.waitForFirstImage.value == true) { if (ffiModel.waitForFirstImage.value == true) {
ffiModel.waitForFirstImage.value = false; ffiModel.waitForFirstImage.value = false;
ffiModel.cancelPendingRestoreTimer();
ffiModel.resetRestartReconnectState(); ffiModel.resetRestartReconnectState();
dialogManager.dismissAll(); dialogManager.dismissAll();
await canvasModel.updateViewStyle(); try {
await canvasModel.updateScrollStyle(); await canvasModel.updateViewStyle();
await canvasModel.initializeEdgeScrollEdgeThickness(); await canvasModel.updateScrollStyle();
for (final cb in imageModel.callbacksOnFirstImage) { await canvasModel.initializeEdgeScrollEdgeThickness();
cb(id); for (final cb in imageModel.callbacksOnFirstImage) {
cb(id);
}
} finally {
_applyPendingMonitorRestore();
} }
} }
} }
void _applyPendingMonitorRestore() {
final restore = ffiModel.pendingMonitorRestore;
ffiModel._cancelPendingMonitorRestore();
if (restore == null || closed) return;
// The display list may have changed since the restore was queued.
final displays = ffiModel.pi.displays;
if ((restore == kAllDisplayValue && displays.isNotEmpty) ||
(restore >= 0 && restore < displays.length)) {
openMonitorInTheSameTab(restore, this, ffiModel.pi,
recordSelection: false, updateCursorPos: false);
}
}
/// Login with [password], choose if the client should [remember] it. /// Login with [password], choose if the client should [remember] it.
void login(String osUsername, String osPassword, SessionID sessionId, void login(String osUsername, String osPassword, SessionID sessionId,
String password, bool remember) { String password, bool remember) {
+34 -2
View File
@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@@ -38,6 +37,10 @@ class TerminalModel with ChangeNotifier {
void Function(int w, int h, int pw, int ph)? onResizeExternal; void Function(int w, int h, int pw, int ph)? onResizeExternal;
/// Called when the terminal session ends (shell exits).
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
VoidCallback? onClosed;
Future<void> _handleInput(String data) async { Future<void> _handleInput(String data) async {
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a // Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'. // real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
@@ -247,6 +250,33 @@ class TerminalModel with ChangeNotifier {
} }
} }
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
if (evt.containsKey('exit_code')) {
final v = evt['exit_code'];
if (v is int) {
// Desktop and mobile send exit_code as an int
return v;
} else if (v is String) {
// Web sends exit_code as a string
final parsed = int.tryParse(v);
if (parsed != null) {
return parsed;
} else {
debugPrint(
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
return 0;
}
} else {
debugPrint(
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
return 0;
}
} else {
debugPrint('[TerminalModel] Event does not contain exit_code');
return 0;
}
}
void handleTerminalResponse(Map<String, dynamic> evt) { void handleTerminalResponse(Map<String, dynamic> evt) {
final String? type = evt['type']; final String? type = evt['type'];
final int evtTerminalId = getTerminalIdFromEvt(evt); final int evtTerminalId = getTerminalIdFromEvt(evt);
@@ -469,10 +499,12 @@ class TerminalModel with ChangeNotifier {
} }
void _handleTerminalClosed(Map<String, dynamic> evt) { void _handleTerminalClosed(Map<String, dynamic> evt) {
final int exitCode = evt['exit_code'] ?? 0; final int exitCode = getExitCodeFromEvt(evt);
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n'); _writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false; _terminalOpened = false;
notifyListeners(); notifyListeners();
// Auto-close the tab/page
onClosed?.call();
} }
void _handleTerminalError(Map<String, dynamic> evt) { void _handleTerminalError(Map<String, dynamic> evt) {
+9
View File
@@ -1914,6 +1914,15 @@ class RustdeskImpl {
throw UnimplementedError("sessionHandleScreenshot"); throw UnimplementedError("sessionHandleScreenshot");
} }
Future<void> sessionSetCommon(
{required UuidValue sessionId, required String key, required String value, dynamic hint}) {
js.context.callMethod('setByName', [
'common',
jsonEncode({'name': key, 'value': value})
]);
return Future.value();
}
String? sessionGetCommonSync( String? sessionGetCommonSync(
{required UuidValue sessionId, {required UuidValue sessionId,
required String key, required String key,
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.
+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 # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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 # 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.8+66 version: 1.4.9+67
environment: environment:
sdk: '^3.1.0' sdk: '^3.1.0'
+2 -2
View File
@@ -89,11 +89,11 @@ BEGIN
BEGIN BEGIN
BLOCK "040904e4" BLOCK "040904e4"
BEGIN BEGIN
VALUE "CompanyName", "Purslane Ltd" "\0" VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
VALUE "FileDescription", "RustDesk Remote Desktop" "\0" VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "rustdesk" "\0" VALUE "InternalName", "rustdesk" "\0"
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
VALUE "OriginalFilename", "rustdesk.exe" "\0" VALUE "OriginalFilename", "rustdesk.exe" "\0"
VALUE "ProductName", "RustDesk" "\0" VALUE "ProductName", "RustDesk" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"
+3 -10
View File
@@ -533,7 +533,7 @@ impl FuseServer {
offset: i64, offset: i64,
size: u32, size: u32,
) -> Result<Vec<u8>, std::io::Error> { ) -> Result<Vec<u8>, std::io::Error> {
// todo: async and concurrent read, generate stream_id per request let request_stream_id = rand::random();
let cb_requested = unsafe { let cb_requested = unsafe {
// convert `size` from u32 to i32 // convert `size` from u32 to i32
// yet with same bit representation // yet with same bit representation
@@ -543,7 +543,7 @@ impl FuseServer {
let (n_position_high, n_position_low) = let (n_position_high, n_position_low) =
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32); ((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
let request = ClipboardFile::FileContentsRequest { let request = ClipboardFile::FileContentsRequest {
stream_id: node.stream_id, stream_id: request_stream_id,
list_index: node.index as i32, list_index: node.index as i32,
dw_flags: 2, dw_flags: 2,
n_position_low, n_position_low,
@@ -573,7 +573,7 @@ impl FuseServer {
stream_id, stream_id,
requested_data, requested_data,
} => { } => {
if stream_id != node.stream_id { if stream_id != request_stream_id {
log::debug!("stream id mismatch, ignore"); log::debug!("stream id mismatch, ignore");
continue; continue;
} }
@@ -611,11 +611,6 @@ struct FuseNode {
/// connection id /// connection id
pub conn_id: i32, pub conn_id: i32,
// todo: use stream_id to identify a FileContents request-reply
// instead of a whole file
/// stream id
pub stream_id: i32,
/// file index in peer's file list /// file index in peer's file list
/// NOTE: /// NOTE:
/// it is NOT the same as inode, this is the index in the file list /// it is NOT the same as inode, this is the index in the file list
@@ -639,7 +634,6 @@ impl FuseNode {
pub fn from_description(inode: Inode, desc: FileDescription) -> Self { pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
Self { Self {
conn_id: desc.conn_id, conn_id: desc.conn_id,
stream_id: rand::random(),
index: inode as usize - 2, index: inode as usize - 2,
name: desc name: desc
.name .name
@@ -656,7 +650,6 @@ impl FuseNode {
pub fn new_root() -> Self { pub fn new_root() -> Self {
Self { Self {
conn_id: 0, conn_id: 0,
stream_id: rand::random(),
index: 0, index: 0,
name: String::from("/"), name: String::from("/"),
parent: None, parent: None,
+352 -21
View File
@@ -4,23 +4,24 @@ use super::filetype::FileDescription;
use crate::{ClipboardFile, CliprdrError}; use crate::{ClipboardFile, CliprdrError};
use cs::FuseServer; use cs::FuseServer;
use fuser::MountOption; use fuser::MountOption;
use hbb_common::{config::APP_NAME, log}; use hbb_common::{config::Config, log};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
path::PathBuf, io,
path::{Path, PathBuf},
sync::{mpsc::Sender, Arc}, sync::{mpsc::Sender, Arc},
time::Duration, time::Duration,
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = { static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client"); let mnt_path = fuse_mount_point("cliprdr-client");
// No need to run `canonicalize()` here. // No need to run `canonicalize()` here.
Arc::new(mnt_path) Arc::new(mnt_path)
}; };
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = { static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server"); let mnt_path = fuse_mount_point("cliprdr-server");
// No need to run `canonicalize()` here. // No need to run `canonicalize()` here.
Arc::new(mnt_path) Arc::new(mnt_path)
}; };
@@ -31,6 +32,21 @@ lazy_static::lazy_static! {
static FUSE_TIMEOUT: Duration = Duration::from_secs(3); static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Debug, PartialEq, Eq)]
enum MountPointState {
HealthyMount,
NotMounted,
StaleMount,
Unknown,
}
fn fuse_mount_point(name: &str) -> String {
let mut path = PathBuf::from(Config::ipc_path(""));
path.pop();
path.push(name);
path.to_string_lossy().to_string()
}
pub fn get_exclude_paths(is_client: bool) -> Arc<String> { pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
if is_client { if is_client {
FUSE_MOUNT_POINT_CLIENT.clone() FUSE_MOUNT_POINT_CLIENT.clone()
@@ -53,8 +69,27 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
} else { } else {
FUSE_CONTEXT_SERVER.lock() FUSE_CONTEXT_SERVER.lock()
}; };
if fuse_context_lock.is_some() { if let Some(ctx) = fuse_context_lock.as_ref() {
return Ok(()); match inspect_mount_point_state(&ctx.mount_point) {
MountPointState::HealthyMount => return Ok(()),
MountPointState::StaleMount | MountPointState::NotMounted => {
log::warn!(
"clipboard FUSE mount {} is disconnected, remounting",
ctx.mount_point.display()
);
let stale_context = fuse_context_lock.take();
drop(fuse_context_lock);
drop(stale_context);
return init_fuse_context(is_client);
}
MountPointState::Unknown => {
log::warn!(
"failed to verify clipboard FUSE mount {}",
ctx.mount_point.display()
);
return Err(CliprdrError::CliprdrInit);
}
}
} }
let mount_point = if is_client { let mount_point = if is_client {
FUSE_MOUNT_POINT_CLIENT.clone() FUSE_MOUNT_POINT_CLIENT.clone()
@@ -63,10 +98,32 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
}; };
let mount_point = std::path::PathBuf::from(&*mount_point); let mount_point = std::path::PathBuf::from(&*mount_point);
match inspect_mount_point_state(&mount_point) {
MountPointState::HealthyMount => {
log::warn!(
"clipboard FUSE mount {} is already active in another context",
mount_point.display()
);
return Err(CliprdrError::ClipboardOccupied);
}
MountPointState::StaleMount => {
log::warn!(
"clipboard FUSE mount {} is stale, cleaning up before remount",
mount_point.display()
);
unmount_fuse_mount_point(&mount_point);
validate_mount_state_after_stale_cleanup(
&mount_point,
inspect_mount_point_state(&mount_point),
)?;
}
MountPointState::Unknown => return Err(CliprdrError::CliprdrInit),
MountPointState::NotMounted => {}
}
let (server, tx) = FuseServer::new(FUSE_TIMEOUT); let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
let server = Arc::new(Mutex::new(server)); let server = Arc::new(Mutex::new(server));
prepare_fuse_mount_point(&mount_point); prepare_fuse_mount_point(&mount_point)?;
let mnt_opts = [ let mnt_opts = [
MountOption::FSName("rustdesk-cliprdr-fs".to_string()), MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
MountOption::NoAtime, MountOption::NoAtime,
@@ -159,35 +216,246 @@ struct FuseContext {
} }
// this function must be called after the main IPC is up // this function must be called after the main IPC is up
fn prepare_fuse_mount_point(mount_point: &PathBuf) { fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> {
use std::{ use std::{
fs::{self, Permissions}, fs::{self, Permissions},
os::unix::prelude::PermissionsExt, os::unix::prelude::PermissionsExt,
}; };
fs::create_dir(mount_point).ok(); if let Some(parent) = mount_point.parent() {
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); reject_symlink_path(parent)?;
if let Err(e) = fs::create_dir_all(parent) {
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
return Err(CliprdrError::CliprdrInit);
}
}
if let Err(e) = std::process::Command::new("umount") reject_symlink_path(mount_point)?;
.arg(mount_point)
.status() let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
{ log::warn!(
log::warn!("umount {:?} may fail: {:?}", mount_point, e); "failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}",
mount_point.display(),
e
);
unmount_fuse_mount_point(mount_point);
fs::create_dir_all(mount_point).map_err(|e| {
log::error!(
"failed to create clipboard FUSE mount point {} after cleanup: {:?}",
mount_point.display(),
e
);
CliprdrError::CliprdrInit
})?;
true
} else {
false
};
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
log::warn!(
"failed to set clipboard FUSE mount point permissions {}: {:?}",
mount_point.display(),
e
);
}
if !recovered_stale_mount {
unmount_fuse_mount_point(mount_point);
}
Ok(())
}
fn inspect_mount_point_state(mount_point: &Path) -> MountPointState {
if ensure_mount_point_path_is_safe(mount_point).is_err() {
return MountPointState::Unknown;
}
inspect_mount_point_state_with(
mount_point,
std::fs::metadata(mount_point),
std::fs::read_to_string("/proc/self/mountinfo"),
)
}
fn validate_mount_state_after_stale_cleanup(
mount_point: &Path,
mount_state: MountPointState,
) -> Result<(), CliprdrError> {
match mount_state {
MountPointState::NotMounted => Ok(()),
MountPointState::HealthyMount => {
log::warn!(
"clipboard FUSE mount {} is still active after stale cleanup",
mount_point.display()
);
Err(CliprdrError::ClipboardOccupied)
}
MountPointState::StaleMount => {
log::warn!(
"clipboard FUSE mount {} is still stale after cleanup",
mount_point.display()
);
Err(CliprdrError::CliprdrInit)
}
MountPointState::Unknown => {
log::warn!(
"failed to verify clipboard FUSE mount {} after cleanup",
mount_point.display()
);
Err(CliprdrError::CliprdrInit)
}
} }
} }
fn uninit_fuse_context_(is_client: bool) { fn inspect_mount_point_state_with<T>(
if is_client { mount_point: &Path,
let _ = FUSE_CONTEXT_CLIENT.lock().take(); metadata_result: io::Result<T>,
} else { mountinfo_result: io::Result<String>,
let _ = FUSE_CONTEXT_SERVER.lock().take(); ) -> MountPointState {
match metadata_result {
Ok(_) => match mountinfo_result {
Ok(mountinfo) => {
if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) {
MountPointState::HealthyMount
} else {
MountPointState::NotMounted
}
}
Err(e) => {
log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e);
MountPointState::Unknown
}
},
Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount,
Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted,
Err(e) => {
log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e);
MountPointState::Unknown
}
} }
} }
fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool {
let mount_point = mount_point.to_string_lossy();
mountinfo.lines().any(|line| {
let mut fields = line.split_whitespace();
let _mount_id = fields.next();
let _parent_id = fields.next();
let _major_minor = fields.next();
let _root = fields.next();
let mount_path = fields.next();
mount_path == Some(mount_point.as_ref())
})
}
fn reject_symlink_metadata_result(
path: &Path,
metadata_result: io::Result<std::fs::Metadata>,
allow_disconnected_mount: bool,
) -> Result<(), CliprdrError> {
match metadata_result {
Ok(metadata) if metadata.file_type().is_symlink() => {
log::warn!("refusing to use symlinked FUSE path {:?}", path);
Err(CliprdrError::CliprdrInit)
}
Ok(_) => Ok(()),
Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => {
log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e);
Err(CliprdrError::CliprdrInit)
}
}
}
fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> {
reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false)
}
fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> {
if let Some(parent) = mount_point.parent() {
reject_symlink_path(parent)?;
}
reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true)
}
fn unmount_fuse_mount_point(mount_point: &Path) {
if ensure_mount_point_path_is_safe(mount_point).is_err() {
log::warn!(
"refusing to unmount unsafe clipboard FUSE mount point {:?}",
mount_point
);
return;
}
if inspect_mount_point_state_with(
mount_point,
std::fs::metadata(mount_point),
std::fs::read_to_string("/proc/self/mountinfo"),
) == MountPointState::NotMounted
{
return;
}
for (program, args) in unmount_command_candidates() {
if run_unmount_command(program, args, mount_point) {
return;
}
}
log::warn!(
"failed to unmount clipboard FUSE mount point {:?}",
mount_point
);
}
fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] {
[
("fusermount3", &["-uz"]),
("fusermount", &["-uz"]),
("umount", &["-l"]),
]
}
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
match std::process::Command::new(program)
.args(args)
.arg(mount_point)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => {
log::debug!(
"{} {:?} exited with status {:?}",
program,
mount_point,
status.code()
);
return false;
}
Err(e) => {
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
return false;
}
}
true
}
fn uninit_fuse_context_(is_client: bool) {
let ctx = {
let mut fuse_context_lock = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
fuse_context_lock.take()
};
drop(ctx);
}
impl Drop for FuseContext { impl Drop for FuseContext {
fn drop(&mut self) { fn drop(&mut self) {
self.session.lock().take().map(|s| s.join()); self.session.lock().take().map(|s| s.join());
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display()); log::info!(
"unmounting clipboard FUSE from {}",
self.mount_point.display()
);
} }
} }
@@ -223,3 +491,66 @@ impl FuseContext {
.collect()) .collect())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, io};
#[cfg(target_family = "unix")]
use std::os::unix::fs::symlink;
#[test]
fn classifies_mount_point_state_from_metadata_and_mountinfo() {
let mount_point = std::env::temp_dir().join(format!(
"rustdesk-fuse-mount-state-test-{}-{}",
std::process::id(),
line!()
));
let mountinfo = format!(
"123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n",
mount_point.display()
);
assert_eq!(
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)),
MountPointState::HealthyMount
);
assert_eq!(
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())),
MountPointState::NotMounted
);
let disconnected_metadata: io::Result<()> =
Err(io::Error::from_raw_os_error(libc::ENOTCONN));
assert_eq!(
inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())),
MountPointState::StaleMount
);
}
#[test]
#[cfg(target_family = "unix")]
fn rejects_symlink_mount_point() {
let base = std::env::temp_dir().join(format!(
"rustdesk-fuse-symlink-test-{}-{}",
std::process::id(),
line!()
));
let mount_parent = base.join("parent");
let mount_point = mount_parent.join("cliprdr-client");
let symlink_target = base.join("symlink-target");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&base).unwrap();
fs::create_dir_all(&mount_parent).unwrap();
fs::create_dir_all(&symlink_target).unwrap();
symlink(&symlink_target, &mount_point).unwrap();
assert!(matches!(
prepare_fuse_mount_point(&mount_point),
Err(CliprdrError::CliprdrInit)
));
let _ = fs::remove_dir_all(&base);
}
}
+46 -12
View File
@@ -192,20 +192,16 @@ impl LocalFile {
}); });
}; };
if offset != self.offset.load(Ordering::Relaxed) { let read_result = if offset != self.offset.load(Ordering::Relaxed) {
handle handle
.seek(std::io::SeekFrom::Start(offset)) .seek(std::io::SeekFrom::Start(offset))
.map_err(|e| CliprdrError::FileError { .and_then(|_| handle.read_exact(buf))
path: self.path.to_string_lossy().to_string(), } else {
err: e, handle.read_exact(buf)
})?; };
if let Err(e) = read_result {
return Err(self.invalidate_handle(e));
} }
handle
.read_exact(buf)
.map_err(|e| CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
let new_offset = offset + (buf.len() as u64); let new_offset = offset + (buf.len() as u64);
self.offset.store(new_offset, Ordering::Relaxed); self.offset.store(new_offset, Ordering::Relaxed);
@@ -217,6 +213,15 @@ impl LocalFile {
Ok(()) Ok(())
} }
fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError {
self.offset.store(0, Ordering::Relaxed);
self.handle = None;
CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err,
}
}
} }
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
@@ -278,7 +283,10 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
#[cfg(test)] #[cfg(test)]
mod file_list_test { mod file_list_test {
use std::{path::PathBuf, sync::atomic::AtomicU64}; use std::{
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use hbb_common::bytes::{BufMut, BytesMut}; use hbb_common::bytes::{BufMut, BytesMut};
@@ -384,4 +392,30 @@ mod file_list_test {
as_bin_parse_test("/test")?; as_bin_parse_test("/test")?;
Ok(()) Ok(())
} }
#[test]
fn read_exact_at_reopens_after_read_failure() -> Result<(), Box<dyn std::error::Error>> {
let file_path = std::env::temp_dir().join(format!(
"rustdesk-clipboard-local-file-{}",
std::process::id()
));
std::fs::write(&file_path, b"")?;
let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?;
file.size = 1;
let mut buf = [0u8; 1];
assert!(file.read_exact_at(&mut buf, 0).is_err());
assert!(file.handle.is_none());
assert_eq!(file.offset.load(Ordering::Relaxed), 0);
std::fs::write(&file_path, [42u8])?;
file.read_exact_at(&mut buf, 0)?;
assert_eq!(buf, [42u8]);
assert!(file.handle.is_none());
std::fs::remove_file(file_path)?;
Ok(())
}
} }
+150 -4
View File
@@ -5,7 +5,7 @@ use hbb_common::{
log, log,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{path::PathBuf, sync::Arc, usize}; use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
lazy_static::lazy_static! { lazy_static::lazy_static! {
// local files are cached, this value should not be changed when copying files // local files are cached, this value should not be changed when copying files
@@ -30,9 +30,35 @@ enum FileContentsRequest {
}, },
} }
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
// or a directory in the selection -- forces sync_files() to rebuild (see below).
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct FileSig {
size: u64,
mtime: Option<SystemTime>,
is_dir: bool,
}
// Stat the top-level selected paths only (no recursion), same order as `files`.
fn fingerprint(files: &[String]) -> Vec<FileSig> {
files
.iter()
.map(|s| match std::fs::metadata(s) {
Ok(mt) => FileSig {
size: mt.len(),
mtime: mt.modified().ok(),
is_dir: mt.is_dir(),
},
Err(_) => FileSig::default(),
})
.collect()
}
#[derive(Default)] #[derive(Default)]
struct ClipFiles { struct ClipFiles {
files: Vec<String>, files: Vec<String>,
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
sigs: Vec<FileSig>,
file_list: Vec<LocalFile>, file_list: Vec<LocalFile>,
first_file_index: usize, first_file_index: usize,
files_pdu: Vec<u8>, files_pdu: Vec<u8>,
@@ -41,12 +67,17 @@ struct ClipFiles {
impl ClipFiles { impl ClipFiles {
fn clear(&mut self) { fn clear(&mut self) {
self.files.clear(); self.files.clear();
self.sigs.clear();
self.file_list.clear(); self.file_list.clear();
self.first_file_index = usize::MAX; self.first_file_index = usize::MAX;
self.files_pdu.clear(); self.files_pdu.clear();
} }
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> { fn sync_files(
&mut self,
clipboard_files: &[String],
sigs: Vec<FileSig>,
) -> Result<(), CliprdrError> {
let clipboard_paths = clipboard_files let clipboard_paths = clipboard_files
.iter() .iter()
.map(|s| PathBuf::from(s)) .map(|s| PathBuf::from(s))
@@ -58,6 +89,7 @@ impl ClipFiles {
.position(|f| !f.path.is_dir()) .position(|f| !f.path.is_dir())
.unwrap_or(usize::MAX); .unwrap_or(usize::MAX);
self.files = clipboard_files.to_vec(); self.files = clipboard_files.to_vec();
self.sigs = sigs;
Ok(()) Ok(())
} }
@@ -258,14 +290,128 @@ pub fn read_file_contents(
} }
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> { pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
// selected (a dir's own mtime doesn't change when a file inside it is edited).
let current = fingerprint(files);
let mut files_lock = CLIP_FILES.lock(); let mut files_lock = CLIP_FILES.lock();
if files_lock.files == files { if files_lock.files == files
&& files_lock.sigs == current
&& !current.iter().any(|sig| sig.is_dir)
{
return Ok(()); return Ok(());
} }
files_lock.sync_files(files)?; files_lock.sync_files(files, current)?;
Ok(files_lock.build_file_list_pdu()) Ok(files_lock.build_file_list_pdu())
} }
pub fn get_file_list_pdu() -> Vec<u8> { pub fn get_file_list_pdu() -> Vec<u8> {
CLIP_FILES.lock().files_pdu.clone() CLIP_FILES.lock().files_pdu.clone()
} }
#[cfg(test)]
mod sig_test {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
struct TmpDir(PathBuf);
impl TmpDir {
fn new(tag: &str) -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut dir = std::env::temp_dir();
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
fs::create_dir_all(&dir).unwrap();
TmpDir(dir)
}
fn join(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
impl Drop for TmpDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn path_str(p: &PathBuf) -> String {
p.to_string_lossy().to_string()
}
#[test]
fn fingerprint_missing_path_is_default() {
let tmp = TmpDir::new("missing");
let missing = path_str(&tmp.join("does_not_exist.bin"));
let sigs = fingerprint(&[missing]);
assert_eq!(sigs.len(), 1);
// A path that can't be stat'd -> default sig, which forces a rebuild.
assert_eq!(sigs[0], FileSig::default());
assert_eq!(sigs[0].mtime, None);
}
#[test]
fn fingerprint_detects_inplace_edit() {
let tmp = TmpDir::new("edit");
let file = tmp.join("a.bin");
fs::write(&file, b"small").unwrap();
let p = path_str(&file);
let before = fingerprint(&[p.clone()]);
// Same content, same path: fingerprint must be stable.
let again = fingerprint(&[p.clone()]);
assert_eq!(before, again);
assert_eq!(before[0].size, 5);
assert!(!before[0].is_dir);
// Edit in place so the file grows.
fs::write(&file, b"much larger contents than before").unwrap();
let after = fingerprint(&[p]);
assert_ne!(before, after);
assert!(after[0].size > before[0].size);
}
#[test]
fn fingerprint_flags_directory() {
let tmp = TmpDir::new("dir");
let sub = tmp.join("subdir");
fs::create_dir_all(&sub).unwrap();
let sigs = fingerprint(&[path_str(&sub)]);
assert_eq!(sigs.len(), 1);
assert!(sigs[0].is_dir);
}
#[test]
fn recopy_after_edit_refreshes_cached_size() {
let tmp = TmpDir::new("recopy");
let file = tmp.join("doc.bin");
fs::write(&file, b"v1").unwrap(); // 2 bytes
let files = vec![path_str(&file)];
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
// reset first (this is the only test that touches the global).
clear_files();
sync_files(&files).unwrap();
{
let cache = CLIP_FILES.lock();
let idx = cache.first_file_index;
assert_eq!(cache.file_list[idx].size, 2);
}
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
// path-only guard early-returned and left the cached size stale at 2.
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
sync_files(&files).unwrap();
{
let cache = CLIP_FILES.lock();
let idx = cache.first_file_index;
assert_eq!(cache.file_list[idx].size, 12);
}
clear_files(); // leave the global clean for other tests
}
}
+168 -36
View File
@@ -41,6 +41,14 @@
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */ /* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
#define WF_CLIPRDR_MAX_STREAMS 16384 #define WF_CLIPRDR_MAX_STREAMS 16384
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
/* Registered format names are string atoms; cap the converted WCHAR name.
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
/* Validates the remote descriptor array size after cItems has been read safely. */ /* Validates the remote descriptor array size after cItems has been read safely. */
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count) static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
@@ -61,6 +69,25 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
return size >= descriptors_size; return size >= descriptors_size;
} }
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
{
size_t i;
if (!value || !len)
return FALSE;
for (i = 0; i <= max_len; i++)
{
if (value[i] == '\0')
{
*len = i;
return TRUE;
}
}
return FALSE;
}
/** /**
* Clipboard Formats * Clipboard Formats
*/ */
@@ -205,6 +232,7 @@ struct _CliprdrStream
FILEDESCRIPTORW m_Dsc; FILEDESCRIPTORW m_Dsc;
void *m_pData; void *m_pData;
UINT32 m_connID; UINT32 m_connID;
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
}; };
typedef struct _CliprdrStream CliprdrStream; typedef struct _CliprdrStream CliprdrStream;
@@ -258,6 +286,9 @@ struct wf_clipboard
char *req_fdata; char *req_fdata;
HANDLE req_fevent; HANDLE req_fevent;
BOOL req_f_received; BOOL req_f_received;
UINT32 req_f_conn_id_expected; // connID of the outstanding request
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
LONG req_f_stream_id_seq; // source of unique per-stream ids
size_t nFiles; size_t nFiles;
size_t file_array_size; size_t file_array_size;
@@ -288,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format); static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
static UINT cliprdr_send_lock(wfClipboard *clipboard); static UINT cliprdr_send_lock(wfClipboard *clipboard);
static UINT cliprdr_send_unlock(wfClipboard *clipboard); static UINT cliprdr_send_unlock(wfClipboard *clipboard);
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
ULONG index, UINT32 flag, DWORD positionhigh, ULONG index, UINT32 flag, DWORD positionhigh,
DWORD positionlow, ULONG request); DWORD positionlow, ULONG request);
@@ -371,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb, static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
ULONG *pcbRead) ULONG *pcbRead)
{ {
int ret; UINT ret;
CliprdrStream *instance = (CliprdrStream *)This; CliprdrStream *instance = (CliprdrStream *)This;
wfClipboard *clipboard; wfClipboard *clipboard;
@@ -384,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart) if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
return S_FALSE; return S_FALSE;
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex, ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
FILECONTENTS_RANGE, instance->m_lOffset.HighPart, FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
instance->m_lOffset.LowPart, cb); instance->m_lOffset.LowPart, cb);
if (ret < 0) if (ret != CHANNEL_RC_OK)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return E_FAIL; return E_FAIL;
}
if (clipboard->req_fsize > cb)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return STG_E_READFAULT;
}
if (clipboard->req_fdata) if (clipboard->req_fdata)
{ {
@@ -601,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
instance->m_pData = pData; instance->m_pData = pData;
instance->m_lOffset.QuadPart = 0; instance->m_lOffset.QuadPart = 0;
instance->m_connID = connID; instance->m_connID = connID;
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES) if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
{ {
@@ -611,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir) if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
{ {
/* get content size of this stream */ /* get content size of this stream */
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance, if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0, instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
8) == CHANNEL_RC_OK) 8) == CHANNEL_RC_OK)
{ {
success = TRUE; success = TRUE;
} }
if (clipboard->req_fdata != NULL) if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
{
LONGLONG sz = 0;
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
if (sz < 0)
success = FALSE;
else
instance->m_lSize.QuadPart = sz;
}
else
{
success = FALSE;
}
if (clipboard->req_fdata)
{ {
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
free(clipboard->req_fdata); free(clipboard->req_fdata);
clipboard->req_fdata = NULL; clipboard->req_fdata = NULL;
} }
@@ -1406,25 +1461,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
return local_format; return local_format;
} }
static void map_ensure_capacity(wfClipboard *clipboard) static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
{ {
size_t old_size;
formatMapping *new_map;
if (!clipboard) if (!clipboard)
return; return FALSE;
if (clipboard->map_size >= clipboard->map_capacity) if (!clipboard->format_mappings)
{ return FALSE;
size_t new_size;
formatMapping *new_map;
new_size = clipboard->map_capacity * 2;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
if (!new_map) if (capacity <= clipboard->map_capacity)
return; return TRUE;
clipboard->format_mappings = new_map; if (capacity > WF_CLIPRDR_MAX_FORMATS ||
clipboard->map_capacity = new_size; capacity > ((size_t)-1) / sizeof(formatMapping))
} return FALSE;
old_size = clipboard->map_capacity;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
if (!new_map)
return FALSE;
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
clipboard->format_mappings = new_map;
clipboard->map_capacity = capacity;
return TRUE;
} }
static BOOL clear_format_map(wfClipboard *clipboard) static BOOL clear_format_map(wfClipboard *clipboard)
@@ -1451,6 +1516,13 @@ static BOOL clear_format_map(wfClipboard *clipboard)
return TRUE; return TRUE;
} }
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
{
clear_format_map(clipboard);
clipboard->copied = FALSE;
return ERROR_INTERNAL_ERROR;
}
static UINT cliprdr_send_tempdir(wfClipboard *clipboard) static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
{ {
CLIPRDR_TEMP_DIRECTORY tempDirectory; CLIPRDR_TEMP_DIRECTORY tempDirectory;
@@ -1729,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem); return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
} }
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
UINT32 flag, DWORD positionhigh, DWORD positionlow, UINT32 flag, DWORD positionhigh, DWORD positionlow,
ULONG nreq) ULONG nreq)
{ {
UINT rc; UINT rc;
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest; CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
@@ -1745,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc; return rc;
} }
clipboard->req_f_received = FALSE; clipboard->req_f_received = FALSE;
clipboard->req_f_conn_id_expected = connID;
clipboard->req_f_stream_id_expected = streamId;
fileContentsRequest.connID = connID; fileContentsRequest.connID = connID;
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system. fileContentsRequest.streamId = streamId;
// But it is OK, because it is only used to check if the stream is the same in
// `wf_cliprdr_server_file_contents_request()` function.
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.listIndex = index; fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag; fileContentsRequest.dwFlags = flag;
fileContentsRequest.nPositionLow = positionlow; fileContentsRequest.nPositionLow = positionlow;
@@ -2443,6 +2514,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
if (!clear_format_map(clipboard)) if (!clear_format_map(clipboard))
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
clipboard->copied = FALSE;
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
return ERROR_INTERNAL_ERROR;
if (formatList->numFormats > 0 && !formatList->formats)
return ERROR_INTERNAL_ERROR;
if (!map_ensure_capacity(clipboard, formatList->numFormats))
return ERROR_INTERNAL_ERROR;
clipboard->copied = TRUE; clipboard->copied = TRUE;
@@ -2450,19 +2531,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
{ {
format = &(formatList->formats[i]); format = &(formatList->formats[i]);
mapping = &(clipboard->format_mappings[i]); mapping = &(clipboard->format_mappings[i]);
/* Do not validate the peer-provided formatId as a Windows registered format.
* It is only a remote protocol ID used when requesting data from the peer.
* For named formats, RegisterClipboardFormatW creates the local Windows
* clipboard ID below, and that local ID is checked before publishing. */
mapping->remote_format_id = format->formatId; mapping->remote_format_id = format->formatId;
if (format->formatName) if (format->formatName)
{ {
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, size_t name_len;
strlen(format->formatName), NULL, 0); int size;
mapping->name = calloc(size + 1, sizeof(WCHAR));
if (mapping->name) if (!wf_cliprdr_bounded_strlen(format->formatName,
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
{ {
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName), return wf_cliprdr_server_format_list_fail(clipboard);
mapping->name, size); }
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
if (name_len == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
NULL, 0);
if (size <= 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
if (!mapping->name)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
mapping->name, size) != size)
{
free(mapping->name);
mapping->name = NULL;
return wf_cliprdr_server_format_list_fail(clipboard);
}
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
if (mapping->local_format_id == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
} }
} }
else else
@@ -2472,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
} }
clipboard->map_size++; clipboard->map_size++;
map_ensure_capacity(clipboard);
} }
if (file_transferring(clipboard)) if (file_transferring(clipboard))
@@ -2923,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
BOOL bIsStreamFile = TRUE; BOOL bIsStreamFile = TRUE;
static LPSTREAM pStreamStc = NULL; static LPSTREAM pStreamStc = NULL;
static UINT32 uStreamIdStc = 0; static UINT32 uStreamIdStc = 0;
static UINT32 uConnIdStc = 0;
wfClipboard *clipboard; wfClipboard *clipboard;
UINT rc = ERROR_INTERNAL_ERROR; UINT rc = ERROR_INTERNAL_ERROR;
UINT sRc; UINT sRc;
@@ -2996,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
vFormatEtc.lindex = fileContentsRequest->listIndex; vFormatEtc.lindex = fileContentsRequest->listIndex;
vFormatEtc.ptd = NULL; vFormatEtc.ptd = NULL;
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc) if ((uStreamIdStc != fileContentsRequest->streamId) ||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
{ {
LPENUMFORMATETC pEnumFormatEtc; LPENUMFORMATETC pEnumFormatEtc;
ULONG CeltFetched; ULONG CeltFetched;
@@ -3027,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
{ {
pStreamStc = vStgMedium.pstm; pStreamStc = vStgMedium.pstm;
uStreamIdStc = fileContentsRequest->streamId; uStreamIdStc = fileContentsRequest->streamId;
uConnIdStc = fileContentsRequest->connID;
bIsStreamFile = TRUE; bIsStreamFile = TRUE;
} }
@@ -3190,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
rc = ERROR_INTERNAL_ERROR; rc = ERROR_INTERNAL_ERROR;
break; break;
} }
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
return CHANNEL_RC_OK;
clipboard->req_fsize = 0; clipboard->req_fsize = 0;
clipboard->req_fdata = NULL; clipboard->req_fdata = NULL;
@@ -3200,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
} }
clipboard->req_fsize = fileContentsResponse->cbRequested; clipboard->req_fsize = fileContentsResponse->cbRequested;
/*
* Keep the zero-size allocation: supported Windows builds use the Microsoft
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
* nonzero length cannot have a NULL data pointer on the normal call path.
*/
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested); clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
if (!clipboard->req_fdata) if (!clipboard->req_fdata)
{ {
+3 -3
View File
@@ -113,11 +113,11 @@ pub enum MouseButton {
/// Scroll up button /// Scroll up button
ScrollUp, ScrollUp,
/// Left right button /// Scroll down button
ScrollDown, ScrollDown,
/// Left right button /// Scroll left button
ScrollLeft, ScrollLeft,
/// Left right button /// Scroll right button
ScrollRight, ScrollRight,
} }
+1 -1
View File
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
// Windows uses uft-16 encoding. We need to check // Windows uses uft-16 encoding. We need to check
// for variable length characters. As such some // for variable length characters. As such some
// characters can be 32 bit long and those are // characters can be 32 bit long and those are
// encoded in such called hight and low surrogates // encoded in so-called high and low surrogates
// each 16 bit wide that needs to be send after // each 16 bit wide that needs to be send after
// another to the SendInput function without // another to the SendInput function without
// being interrupted by "keyup" // being interrupted by "keyup"
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.4.8" version = "1.4.9"
edition = "2021" edition = "2021"
description = "RustDesk Remote Desktop" description = "RustDesk Remote Desktop"
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]} native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk" ProductName = "RustDesk"
OriginalFilename = "rustdesk.exe" OriginalFilename = "rustdesk.exe"
FileDescription = "RustDesk Remote Desktop" FileDescription = "RustDesk Remote Desktop"
+22 -2
View File
@@ -79,6 +79,10 @@ mod webrtc {
} }
} }
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
(threads as f64).log2().ceil() as _
}
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t { fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
use aom_superblock_size::*; use aom_superblock_size::*;
let resolution = width * height; let resolution = width * height;
@@ -160,8 +164,7 @@ mod webrtc {
} else { } else {
AV1E_SET_TILE_COLUMNS AV1E_SET_TILE_COLUMNS
}; };
// Failed on android call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
call_ctl!(ctx, AV1E_SET_ROW_MT, 1); call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0); call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0); call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
@@ -197,6 +200,23 @@ mod webrtc {
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
use std::os::raw::c_uint;
#[test]
fn tile_log2_uses_c_uint_and_rounds_up() {
let one_thread: c_uint = tile_log2(1);
let three_threads: c_uint = tile_log2(3);
let max_threads: c_uint = tile_log2(64);
assert_eq!(one_thread, 0);
assert_eq!(three_threads, 2);
assert_eq!(max_threads, 6);
}
}
} }
impl EncoderApi for AomEncoder { impl EncoderApi for AomEncoder {
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=rustdesk pkgname=rustdesk
pkgver=1.4.8 pkgver=1.4.9
pkgrel=0 pkgrel=0
epoch= epoch=
pkgdesc="" pkgdesc=""
+2 -2
View File
@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520 \ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1 \par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; \lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; \lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; \lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}} \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
+2 -2
View File
@@ -85,7 +85,7 @@ def make_parser():
"-m", "-m",
"--manufacturer", "--manufacturer",
type=str, type=str,
default="PURSLANE", default="Purslane Tech Pte. Ltd.",
help="The app manufacturer.", help="The app manufacturer.",
) )
return parser return parser
@@ -499,7 +499,7 @@ def update_license_file(app_name):
license_content = f.read() license_content = f.read()
license_content = license_content.replace("website rustdesk.com and other ", "") license_content = license_content.replace("website rustdesk.com and other ", "")
license_content = license_content.replace("RustDesk", app_name) license_content = license_content.replace("RustDesk", app_name)
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE) license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
with open(license_file, "w", encoding="utf-8") as f: with open(license_file, "w", encoding="utf-8") as f:
f.write(license_content) f.write(license_content)
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.8 Version: 1.4.9
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.8 Version: 1.4.9
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0
+1 -1
View File
@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.8 Version: 1.4.9
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0
+1 -1
View File
@@ -45,7 +45,7 @@ pre_start()
return 0 return 0
} }
# When loging out from the interactive shell, the execution sequence is: # When logging out from the interactive shell, the execution sequence is:
# #
# IF ~/.bash_logout exists THEN # IF ~/.bash_logout exists THEN
# execute ~/.bash_logout # execute ~/.bash_logout
@@ -0,0 +1,7 @@
set(VCPKG_TARGET_ARCHITECTURE arm)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Android)
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
set(VCPKG_MAKE_BUILD_TRIPLET "--host=armv7a-linux-androideabi")
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=armeabi-v7a -DANDROID_ARM_NEON=ON)
+7
View File
@@ -0,0 +1,7 @@
set(VCPKG_TARGET_ARCHITECTURE arm64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Android)
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
set(VCPKG_MAKE_BUILD_TRIPLET "--host=aarch64-linux-android")
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=arm64-v8a)
+7
View File
@@ -0,0 +1,7 @@
set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Android)
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
set(VCPKG_MAKE_BUILD_TRIPLET "--host=x86_64-linux-android")
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86_64)
+7
View File
@@ -0,0 +1,7 @@
set(VCPKG_TARGET_ARCHITECTURE x86)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Android)
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
set(VCPKG_MAKE_BUILD_TRIPLET "--host=i686-linux-android")
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86)
-199
View File
@@ -1,199 +0,0 @@
use crate::client::*;
use async_trait::async_trait;
use hbb_common::{
config::PeerConfig,
config::READ_TIMEOUT,
futures::{SinkExt, StreamExt},
log,
message_proto::*,
protobuf::Message as _,
rendezvous_proto::ConnType,
tokio::{self, sync::mpsc},
Stream,
};
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct Session {
id: String,
lc: Arc<RwLock<LoginConfigHandler>>,
sender: mpsc::UnboundedSender<Data>,
password: String,
}
impl Session {
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
let mut password = "".to_owned();
if PeerConfig::load(id).password.is_empty() {
match rpassword::prompt_password("Enter password: ") {
Ok(p) => password = p,
Err(e) => {
log::error!("Failed to read password: {:?}", e);
password = "".to_owned();
}
}
}
let session = Self {
id: id.to_owned(),
sender,
password,
lc: Default::default(),
};
session.lc.write().unwrap().initialize(
id.to_owned(),
ConnType::PORT_FORWARD,
None,
false,
None,
None,
);
session
}
}
#[async_trait]
impl Interface for Session {
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
return self.lc.clone();
}
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
match msgtype {
"input-password" => {
self.sender
.send(Data::Login((self.password.clone(), true)))
.ok();
}
"re-input-password" => {
log::error!("{}: {}", title, text);
match rpassword::prompt_password("Enter password: ") {
Ok(password) => {
let login_data = Data::Login((password, true));
self.sender.send(login_data).ok();
}
Err(e) => {
log::error!("reinput password failed, {:?}", e);
}
}
}
msg if msg.contains("error") => {
log::error!("{}: {}: {}", msgtype, title, text);
}
_ => {
log::info!("{}: {}: {}", msgtype, title, text);
}
}
}
fn handle_login_error(&self, err: &str) -> bool {
handle_login_error(self.lc.clone(), err, self)
}
fn handle_peer_info(&self, pi: PeerInfo) {
self.lc.write().unwrap().handle_peer_info(&pi);
}
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
log::info!(
"password={}",
hbb_common::password_security::temporary_password()
);
handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
}
async fn handle_login_from_ui(
&self,
os_username: String,
os_password: String,
password: String,
remember: bool,
peer: &mut Stream,
) {
handle_login_from_ui(
self.lc.clone(),
os_username,
os_password,
password,
remember,
peer,
)
.await;
}
async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) {
handle_test_delay(t, peer).await;
}
fn send(&self, data: Data) {
self.sender.send(data).ok();
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn connect_test(id: &str, key: String, token: String) {
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
let handler = Session::new(&id, sender);
match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await {
Err(err) => {
log::error!("Failed to connect {}: {}", &id, err);
}
Ok((mut stream, direct)) => {
log::info!("direct: {}", direct);
// rpassword::prompt_password("Input anything to exit").ok();
loop {
tokio::select! {
res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res {
Err(_) => {
log::error!("Timeout");
break;
}
Ok(Some(Ok(bytes))) => {
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
match msg_in.union {
Some(message::Union::Hash(hash)) => {
log::info!("Got hash");
break;
}
_ => {}
}
}
}
_ => {}
}
}
}
}
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn start_one_port_forward(
id: String,
port: i32,
remote_host: String,
remote_port: i32,
key: String,
token: String,
) {
crate::common::test_rendezvous_server();
crate::common::test_nat_type();
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
let handler = Session::new(&id, sender);
if let Err(err) = crate::port_forward::listen(
handler.id.clone(),
handler.password.clone(),
port,
handler.clone(),
receiver,
&key,
&token,
handler.lc.clone(),
remote_host,
remote_port,
)
.await
{
log::error!("Failed to listen on {}: {}", port, err);
}
log::info!("port forward (:{}) exit", port);
}
+33 -8
View File
@@ -941,21 +941,23 @@ impl Client {
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
fn try_stop_clipboard() { fn try_stop_clipboard() {
// There's a bug here. // Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
// If session is closed by the peer, `has_sessions_running()` will always return true. // should block clipboard cleanup.
// It's better to check if the active session number.
// But it's not a problem, because the clipboard thread does not consume CPU.
//
// If we want to fix it, we can add a flag to indicate if session is active.
// But I think it's not necessary to introduce complexity at this point.
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
return; return;
} }
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
CLIPBOARD_STATE.lock().unwrap().running = false; CLIPBOARD_STATE.lock().unwrap().running = false;
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
crate::clipboard::ClipboardSide::Client,
0,
) {
log::error!("Failed to empty client clipboard files: {}", e);
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
clipboard::platform::unix::fuse::uninit_fuse_context(true); clipboard::platform::unix::fuse::uninit_fuse_context(true);
} }
@@ -3790,6 +3792,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
#[derive(Clone)] #[derive(Clone)]
pub enum Data { pub enum Data {
Close, Close,
RejectInsecureConnection,
Login((String, String, String, bool)), Login((String, String, String, bool)),
Message(Message), Message(Message),
SendFiles((i32, JobType, String, String, i32, bool, bool)), SendFiles((i32, JobType, String, String, i32, bool, bool)),
@@ -3813,11 +3816,33 @@ pub enum Data {
ElevateWithLogon(String, String), ElevateWithLogon(String, String),
NewVoiceCall, NewVoiceCall,
CloseVoiceCall, CloseVoiceCall,
ContinueInsecureConnection,
ResetDecoder(Option<usize>), ResetDecoder(Option<usize>),
RenameFile((i32, String, String, bool)), RenameFile((i32, String, String, bool)),
TakeScreenshot((i32, String)), TakeScreenshot((i32, String)),
} }
pub async fn confirm_insecure_connection(
interface: &impl Interface,
receiver: &mut UnboundedReceiver<Data>,
) -> bool {
interface.msgbox(
"insecure-connection-nocancel-hasclose",
"Insecure Connection",
"conn-e2ee-unavailable-tip",
"",
);
while let Some(data) = receiver.recv().await {
match data {
Data::ContinueInsecureConnection => return true,
Data::RejectInsecureConnection => return false,
Data::Close => return false,
_ => {}
}
}
false
}
/// Keycode for key events. /// Keycode for key events.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Key { pub enum Key {
-6
View File
@@ -6,7 +6,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn get_home_dir(&self) -> String { fn get_home_dir(&self) -> String {
@@ -16,7 +15,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn get_next_job_id(&self) -> i32 { fn get_next_job_id(&self) -> i32 {
@@ -26,7 +24,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn update_next_job_id(&self, id: i32) { fn update_next_job_id(&self, id: i32) {
@@ -36,7 +33,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value {
@@ -90,7 +86,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn confirm_delete_files(&self, id: i32, file_num: i32) { fn confirm_delete_files(&self, id: i32, file_num: i32) {
@@ -100,7 +95,6 @@ pub trait FileManager: Interface {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
feature = "cli",
feature = "flutter" feature = "flutter"
)))] )))]
fn set_no_confirm(&self, id: i32) { fn set_no_confirm(&self, id: i32) {
+27 -2
View File
@@ -14,6 +14,7 @@ use crate::{
// Empirical no-data window before exposing the restart reconnect state to the UI. // 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. // 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); const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30);
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
#[cfg(any( #[cfg(any(
@@ -183,8 +184,20 @@ impl<T: InvokeUiSession> Remote<T> {
.lock() .lock()
.unwrap() .unwrap()
.set_connected(); .set_connected();
let is_secured = peer.is_secured();
self.handler self.handler
.set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready .set_connection_type(is_secured, direct, stream_type); // flutter -> connection_ready
if !is_secured
&& !crate::common::is_direct_ip_access(&self.handler.get_id())
&& !client::confirm_insecure_connection(&self.handler, &mut self.receiver).await
{
self.send_close_reason(&mut peer, "").await;
if kcp.is_some() {
tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
}
self.handle_disconnected(round);
return;
}
self.handler.update_direct(Some(direct)); self.handler.update_direct(Some(direct));
if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA {
self.handler self.handler
@@ -338,13 +351,17 @@ impl<T: InvokeUiSession> Remote<T> {
self.send_close_reason(&mut peer, "kcp").await; self.send_close_reason(&mut peer, "kcp").await;
// KCP does not send messages immediately, so wait to ensure the last message is sent. // KCP does not send messages immediately, so wait to ensure the last message is sent.
// 1ms works in my test, but 30ms is more reliable. // 1ms works in my test, but 30ms is more reliable.
tokio::time::sleep(Duration::from_millis(30)).await; tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
} }
} }
Err(err) => { Err(err) => {
self.handler.on_establish_connection_error(err.to_string()); self.handler.on_establish_connection_error(err.to_string());
} }
} }
self.handle_disconnected(round);
}
fn handle_disconnected(&self, round: u32) {
// set_disconnected_ok is used to check if new connection round is started. // set_disconnected_ok is used to check if new connection round is started.
let _set_disconnected_ok = self let _set_disconnected_ok = self
.handler .handler
@@ -360,6 +377,8 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
if self.handler.is_default() && _set_disconnected_ok { if self.handler.is_default() && _set_disconnected_ok {
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
// unmounted. Keep this async path for other file-clipboard platforms.
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
} }
} }
@@ -1088,6 +1107,9 @@ impl<T: InvokeUiSession> Remote<T> {
} }
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
if self.handler.is_view_camera() {
return;
}
if !self.peer_info.is_support_virtual_display() { if !self.peer_info.is_support_virtual_display() {
return; return;
} }
@@ -1109,6 +1131,9 @@ impl<T: InvokeUiSession> Remote<T> {
} }
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) { async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
if self.handler.is_view_camera() {
return;
}
let lc = self.handler.lc.read().unwrap(); let lc = self.handler.lc.read().unwrap();
if lc.version >= hbb_common::get_version_number("1.2.4") if lc.version >= hbb_common::get_version_number("1.2.4")
&& lc.get_toggle_option("privacy-mode") && lc.get_toggle_option("privacy-mode")
+40 -31
View File
@@ -151,41 +151,50 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
std::thread::spawn(move || { std::thread::spawn(move || {
let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
if ctx.is_none() { log::error!("Failed to empty clipboard files: {}", e);
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
return;
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
context.empty_clipboard(_conn_id).ok();
Ok(())
})
.ok();
}
} }
}); });
} }
#[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
bail!("Failed to create clipboard context: {}", e);
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
if !context.empty_clipboard(_conn_id)? {
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
}
Ok(())
})?;
}
}
Ok(())
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
+20 -12
View File
@@ -332,12 +332,16 @@ pub mod unix_file_clip {
log::debug!("format data response: msg_flags: {}", msg_flags); log::debug!("format data response: msg_flags: {}", msg_flags);
if msg_flags != 0x1 { if msg_flags != 0x1 {
// return failure message? log::error!(
"peer reported clipboard format data failure: {}",
msg_flags
);
return vec![];
} }
log::debug!("parsing file descriptors"); log::debug!("parsing file descriptors");
if fuse::init_fuse_context(true).is_ok() { match fuse::init_fuse_context(side == ClipboardSide::Client) {
match fuse::format_data_response_to_urls( Ok(()) => match fuse::format_data_response_to_urls(
side == ClipboardSide::Client, side == ClipboardSide::Client,
format_data, format_data,
conn_id, conn_id,
@@ -348,9 +352,10 @@ pub mod unix_file_clip {
Err(e) => { Err(e) => {
log::error!("failed to parse file descriptors: {:?}", e); log::error!("failed to parse file descriptors: {:?}", e);
} }
},
Err(e) => {
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
} }
} else {
// send error message to server
} }
} }
ClipboardFile::FileContentsRequest { ClipboardFile::FileContentsRequest {
@@ -386,6 +391,7 @@ pub mod unix_file_clip {
ClipboardFile::FileContentsResponse { ClipboardFile::FileContentsResponse {
msg_flags, msg_flags,
stream_id, stream_id,
requested_data,
.. ..
} => { } => {
log::debug!( log::debug!(
@@ -393,13 +399,15 @@ pub mod unix_file_clip {
msg_flags, msg_flags,
stream_id, stream_id,
); );
if fuse::init_fuse_context(true).is_ok() { let response = ClipboardFile::FileContentsResponse {
hbb_common::allow_err!(fuse::handle_file_content_response( msg_flags,
side == ClipboardSide::Client, stream_id,
clip requested_data,
)); };
} else { if let Err(e) =
// send error message to server fuse::handle_file_content_response(side == ClipboardSide::Client, response)
{
log::error!("failed to handle file contents response: {:?}", e);
} }
} }
ClipboardFile::NotifyCallback { ClipboardFile::NotifyCallback {
+6 -3
View File
@@ -764,15 +764,14 @@ async fn test_rendezvous_server_() {
Config::reset_online(); Config::reset_online();
} }
// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
pub fn test_rendezvous_server() { pub fn test_rendezvous_server() {
std::thread::spawn(test_rendezvous_server_); std::thread::spawn(test_rendezvous_server_);
} }
pub fn refresh_rendezvous_server() { pub fn refresh_rendezvous_server() {
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] #[cfg(any(target_os = "android", target_os = "ios"))]
test_rendezvous_server(); test_rendezvous_server();
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
std::thread::spawn(|| { std::thread::spawn(|| {
if crate::ipc::test_rendezvous_server().is_err() { if crate::ipc::test_rendezvous_server().is_err() {
test_rendezvous_server(); test_rendezvous_server();
@@ -2620,6 +2619,10 @@ pub fn get_control_permission(
} }
} }
pub fn is_direct_ip_access(peer: &str) -> bool {
hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+15 -2
View File
@@ -1437,7 +1437,7 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
pub fn update_text_clipboard_required() { pub fn update_text_clipboard_required() {
let is_required = sessions::get_sessions() let is_required = sessions::get_sessions()
.iter() .iter()
.any(|s| s.is_text_clipboard_required()); .any(|s| s.is_default() && s.is_text_clipboard_required());
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
Client::set_is_text_clipboard_required(is_required); Client::set_is_text_clipboard_required(is_required);
@@ -1447,13 +1447,16 @@ pub fn update_text_clipboard_required() {
pub fn update_file_clipboard_required() { pub fn update_file_clipboard_required() {
let is_required = sessions::get_sessions() let is_required = sessions::get_sessions()
.iter() .iter()
.any(|s| s.is_file_clipboard_required()); .any(|s| s.is_default() && s.is_file_clipboard_required());
Client::set_is_file_clipboard_required(is_required); Client::set_is_file_clipboard_required(is_required);
} }
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
pub fn send_clipboard_msg(msg: Message, _is_file: bool) { pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
for s in sessions::get_sessions() { for s in sessions::get_sessions() {
if !s.is_default() {
continue;
}
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
if _is_file { if _is_file {
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version) if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
@@ -2297,6 +2300,16 @@ pub mod sessions {
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
}) })
} }
#[inline]
#[cfg(not(target_os = "ios"))]
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
*r#type == conn_type
&& s.session_handlers.read().unwrap().len() != 0
&& s.connection_round_state.lock().unwrap().is_connected()
})
}
} }
pub(super) mod async_tasks { pub(super) mod async_tasks {
+29 -3
View File
@@ -1026,7 +1026,7 @@ pub fn main_set_option(key: String, value: String) {
set_option(key, value.clone()); set_option(key, value.clone());
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
crate::rendezvous_mediator::RendezvousMediator::restart(); crate::rendezvous_mediator::RendezvousMediator::restart();
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] #[cfg(any(target_os = "android", target_os = "ios"))]
crate::common::test_rendezvous_server(); crate::common::test_rendezvous_server();
} else { } else {
set_option(key, value.clone()); set_option(key, value.clone());
@@ -1224,9 +1224,14 @@ pub fn main_set_local_option(key: String, value: String) {
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
set_local_option(key, value.clone()); set_local_option(key, value.clone());
let is_render_target =
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
if is_texture_render_key { if is_texture_render_key {
let session_event = [("v", &value)]; let session_event = [("v", &value)];
for session in sessions::get_sessions() { for session in sessions::get_sessions() {
if !is_render_target(&session) {
continue;
}
session.push_event("use_texture_render", &session_event, &[]); session.push_event("use_texture_render", &session_event, &[]);
session.use_texture_render_changed(); session.use_texture_render_changed();
session.ui_handler.update_use_texture_render(); session.ui_handler.update_use_texture_render();
@@ -1234,6 +1239,9 @@ pub fn main_set_local_option(key: String, value: String) {
} }
if is_d3d_render_key { if is_d3d_render_key {
for session in sessions::get_sessions() { for session in sessions::get_sessions() {
if !is_render_target(&session) {
continue;
}
session.update_supported_decodings(); session.update_supported_decodings();
} }
} }
@@ -2852,8 +2860,16 @@ pub fn main_get_common(key: String) -> String {
crate::platform::windows::is_msi_installed(), crate::platform::windows::is_msi_installed(),
crate::common::is_custom_client(), crate::common::is_custom_client(),
) { ) {
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"), (Ok(true), false) => match crate::platform::windows::release_arch_suffix() {
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"), Some(arch) => format!("rustdesk-{_version}-{arch}.msi"),
None => "error:unsupported".to_owned(),
},
(Ok(true), true) | (Ok(false), _) => {
match crate::platform::windows::release_arch_suffix() {
Some(arch) => format!("rustdesk-{_version}-{arch}.exe"),
None => "error:unsupported".to_owned(),
}
}
(Err(e), _) => { (Err(e), _) => {
log::error!("Failed to check if is msi: {}", e); log::error!("Failed to check if is msi: {}", e);
format!("error:update-failed-check-msi-tip") format!("error:update-failed-check-msi-tip")
@@ -3012,6 +3028,16 @@ pub fn main_set_common(_key: String, _value: String) {
} }
} }
pub fn session_set_common(session_id: SessionID, key: String, value: String) {
if let Some(s) = sessions::get_session_by_session_id(&session_id) {
if key == "continue-insecure-connection"
{
s.continue_insecure_connection(value == "Y");
return;
}
}
}
pub fn session_get_common_sync( pub fn session_get_common_sync(
session_id: SessionID, session_id: SessionID,
key: String, key: String,
+2 -2
View File
@@ -9,8 +9,8 @@ mod http_client;
pub mod record_upload; pub mod record_upload;
pub mod sync; pub mod sync;
pub use http_client::{ pub use http_client::{
create_http_client_async, create_http_client_async_with_url, create_http_client_with_url, create_http_client_async, create_http_client_async_with_url_strict,
get_url_for_tls, create_http_client_with_url, create_http_client_with_url_strict, get_url_for_tls,
}; };
#[derive(Debug)] #[derive(Debug)]
+2 -2
View File
@@ -1,4 +1,4 @@
use super::create_http_client_async_with_url; use super::create_http_client_async_with_url_strict;
use hbb_common::{ use hbb_common::{
bail, bail,
lazy_static::lazy_static, lazy_static::lazy_static,
@@ -167,7 +167,7 @@ async fn do_download(
auto_del_dur: Option<Duration>, auto_del_dur: Option<Duration>,
mut rx_cancel: UnboundedReceiver<()>, mut rx_cancel: UnboundedReceiver<()>,
) -> ResultType<bool> { ) -> ResultType<bool> {
let client = create_http_client_async_with_url(&url).await; let client = create_http_client_async_with_url_strict(&url).await?;
let mut is_all_downloaded = false; let mut is_all_downloaded = false;
tokio::select! { tokio::select! {
+55
View File
@@ -1,5 +1,6 @@
use hbb_common::{ use hbb_common::{
async_recursion::async_recursion, async_recursion::async_recursion,
bail,
config::{Config, Socks5Server}, config::{Config, Socks5Server},
log::{self, info}, log::{self, info},
proxy::{Proxy, ProxyScheme}, proxy::{Proxy, ProxyScheme},
@@ -7,6 +8,7 @@ use hbb_common::{
get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache, get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache,
TlsType, TlsType,
}, },
ResultType,
}; };
use reqwest::{blocking::Client as SyncClient, Client as AsyncClient}; use reqwest::{blocking::Client as SyncClient, Client as AsyncClient};
@@ -137,6 +139,32 @@ pub fn create_http_client_with_url(url: &str) -> SyncClient {
) )
} }
pub fn create_http_client_with_url_strict(url: &str) -> ResultType<SyncClient> {
let parsed_url = url::Url::parse(url)?;
if parsed_url.scheme() != "https" {
bail!("Strict HTTP client requires HTTPS: {}", url);
}
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(url, &proxy_conf);
let cached_tls_type = get_cached_tls_type(tls_url);
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let can_reuse_cached_probe =
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
let tls_type = if can_reuse_cached_probe {
cached_tls_type.unwrap_or(TlsType::Rustls)
} else {
TlsType::Rustls
};
Ok(create_http_client_with_url_(
url,
tls_url,
tls_type,
can_reuse_cached_probe,
Some(false),
Some(false),
))
}
fn create_http_client_with_url_( fn create_http_client_with_url_(
url: &str, url: &str,
tls_url: &str, tls_url: &str,
@@ -247,6 +275,33 @@ pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient {
.await .await
} }
pub async fn create_http_client_async_with_url_strict(url: &str) -> ResultType<AsyncClient> {
let parsed_url = url::Url::parse(url)?;
if parsed_url.scheme() != "https" {
bail!("Strict HTTP client requires HTTPS: {}", url);
}
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(url, &proxy_conf);
let cached_tls_type = get_cached_tls_type(tls_url);
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let can_reuse_cached_probe =
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
let tls_type = if can_reuse_cached_probe {
cached_tls_type.unwrap_or(TlsType::Rustls)
} else {
TlsType::Rustls
};
Ok(create_http_client_async_with_url_(
url,
tls_url,
tls_type,
can_reuse_cached_probe,
Some(false),
Some(false),
)
.await)
}
#[async_recursion] #[async_recursion]
async fn create_http_client_async_with_url_( async fn create_http_client_async_with_url_(
url: &str, url: &str,
+4 -4
View File
@@ -2,7 +2,7 @@
use crate::flutter; use crate::flutter;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk}; use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(feature = "flutter"))]
use crate::ui::CUR_SESSION; use crate::ui::CUR_SESSION;
use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::ui_session_interface::{InvokeUiSession, Session};
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -469,7 +469,7 @@ static mut IS_LEFT_OPTION_DOWN: bool = false;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn get_keyboard_mode() -> String { fn get_keyboard_mode() -> String {
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(feature = "flutter"))]
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
return session.get_keyboard_mode(); return session.get_keyboard_mode();
} }
@@ -991,7 +991,7 @@ pub fn event_to_key_events(
} }
pub fn send_key_event(key_event: &KeyEvent) { pub fn send_key_event(key_event: &KeyEvent) {
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(feature = "flutter"))]
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
session.send_key_event(key_event); session.send_key_event(key_event);
} }
@@ -1003,7 +1003,7 @@ pub fn send_key_event(key_event: &KeyEvent) {
} }
pub fn get_peer_platform() -> String { pub fn get_peer_platform() -> String {
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(feature = "flutter"))]
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
return session.peer_platform(); return session.peer_platform();
} }
+67 -7
View File
@@ -103,15 +103,29 @@ pub const LANGS: &[(&str, &str)] = &[
("gu", "ગુજરાતી"), ("gu", "ગુજરાતી"),
]; ];
#[cfg(not(any(target_os = "android", target_os = "ios")))] pub(crate) fn cjk_ui_unavailable() -> bool {
pub fn translate(name: String) -> String { cfg!(all(
let locale = sys_locale::get_locale().unwrap_or_default(); target_os = "linux",
translate_locale(name, &locale) target_arch = "aarch64",
feature = "flutter"
))
} }
pub fn translate_locale(name: String, locale: &str) -> String { pub(crate) fn is_cjk_lang(lang_or_locale: &str) -> bool {
let lang = lang_or_locale
.split(|c| c == '-' || c == '_')
.next()
.unwrap_or_default()
.to_lowercase();
matches!(lang.as_str(), "zh" | "ja" | "ko")
}
fn resolve_lang(saved_lang: &str, locale: &str, cjk_fallback: bool) -> String {
let locale = locale.to_lowercase(); let locale = locale.to_lowercase();
let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase(); let mut lang = saved_lang.to_lowercase();
if cjk_fallback && is_cjk_lang(&lang) {
return "en".to_owned();
}
if lang.is_empty() { if lang.is_empty() {
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
if locale.starts_with("zh") { if locale.starts_with("zh") {
@@ -131,7 +145,25 @@ pub fn translate_locale(name: String, locale: &str) -> String {
.unwrap_or_default() .unwrap_or_default()
.to_owned(); .to_owned();
} }
let lang = lang.to_lowercase(); if cjk_fallback && is_cjk_lang(&lang) {
"en".to_owned()
} else {
lang
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default();
translate_locale(name, &locale)
}
pub fn translate_locale(name: String, locale: &str) -> String {
let lang = resolve_lang(
&hbb_common::config::LocalConfig::get_option("lang"),
locale,
cjk_ui_unavailable(),
);
let m = match lang.as_str() { let m = match lang.as_str() {
"fr" => fr::T.deref(), "fr" => fr::T.deref(),
"zh-cn" => cn::T.deref(), "zh-cn" => cn::T.deref(),
@@ -275,4 +307,32 @@ mod test {
("{} times {4} makes {8}".to_string(), Some("2".to_string())) ("{} times {4} makes {8}".to_string(), Some("2".to_string()))
); );
} }
#[test]
fn test_resolve_lang_forces_english_for_saved_cjk_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", true), "en");
assert_eq!(f("zh-tw", "en-US", true), "en");
assert_eq!(f("ja", "en-US", true), "en");
assert_eq!(f("ko", "en-US", true), "en");
}
#[test]
fn test_resolve_lang_forces_english_for_cjk_locale_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("", "zh_CN", true), "en");
assert_eq!(f("", "ja-JP", true), "en");
assert_eq!(f("", "ko_KR", true), "en");
}
#[test]
fn test_resolve_lang_preserves_cjk_when_target_allows_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", false), "zh-cn");
assert_eq!(f("", "zh_TW", false), "zh-tw");
assert_eq!(f("", "ja-JP", false), "ja");
}
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "اتصال الوسيط"), ("Relay Connection", "اتصال الوسيط"),
("Secure Connection", "اتصال آمن"), ("Secure Connection", "اتصال آمن"),
("Insecure Connection", "اتصال غير آمن"), ("Insecure Connection", "اتصال غير آمن"),
("Continue", ""),
("Scale original", "المقياس الأصلي"), ("Scale original", "المقياس الأصلي"),
("Scale adaptive", "مقياس التكيف"), ("Scale adaptive", "مقياس التكيف"),
("General", "عام"), ("General", "عام"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "تبديل الشاشة"), ("Switch display", "تبديل الشاشة"),
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"), ("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"), ("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
("All monitors", "جميع الشاشات"),
("#{} monitor", "الشاشة رقم {}"),
("conn-e2ee-unavailable-tip", "تعذر التحقق من التشفير من طرف إلى طرف.\nقد يكون الجهاز البعيد ما يزال قيد الإعداد. حاول مرة أخرى لاحقًا.\nإذا استمر حدوث ذلك، فقد يكون الخادم غير موثوق به.\nهل تريد المتابعة على أي حال؟"),
("ID whitelisting", "القائمة البيضاء للمعرفات"),
("Use ID whitelisting", "استخدام القائمة البيضاء للمعرفات"),
("id_whitelist_tip", "فقط المعرفات في القائمة البيضاء تستطيع الوصول لي"),
("id_whitelist_wildcard_tip", "أحرف البدل مدعومة: '*' يطابق أي عدد من الأحرف، '?' يطابق حرفاً واحداً بالضبط"),
("Invalid ID", "معرف غير صحيح"),
("Your ID is blocked by the peer", "تم حظر معرفك من قبل الطرف الآخر"),
("Your ip is blocked by the peer", "تم حظر عنوان IP الخاص بك من قبل الطرف الآخر"),
("id_whitelist_caveat_tip", "يتم الإبلاغ عن المعرف من قبل العميل المتصل. القائمة البيضاء تقلل من التعرض ولا تغني عن كلمة المرور أو 2FA"),
("whitelist_cidr_tip", "يتم دعم صيغة CIDR، مثال: 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Рэтрансляванае падключэнне"), ("Relay Connection", "Рэтрансляванае падключэнне"),
("Secure Connection", "Бяспечнае падключэнне"), ("Secure Connection", "Бяспечнае падключэнне"),
("Insecure Connection", "Нябяспечнае падключэнне"), ("Insecure Connection", "Нябяспечнае падключэнне"),
("Continue", ""),
("Scale original", "Арыгінальны маштаб"), ("Scale original", "Арыгінальны маштаб"),
("Scale adaptive", "Адаптыўны маштаб"), ("Scale adaptive", "Адаптыўны маштаб"),
("General", "Агульныя"), ("General", "Агульныя"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Пераключыць дысплэй"), ("Switch display", "Пераключыць дысплэй"),
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"), ("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"), ("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
("All monitors", "Усе манітори"),
("#{} monitor", "Манітор {}"),
("conn-e2ee-unavailable-tip", "Не ўдалося праверыць скразное шыфраванне.\nАддаленая прылада, магчыма, яшчэ наладжваецца. Паспрабуйце пазней.\nКалі гэта будзе паўтарацца, сервер можа быць ненадзейным.\nУсё роўна працягнуць?"),
("ID whitelisting", "Спіс дазволеных ID"),
("Use ID whitelisting", "Выкарыстоўваць белы спіс ID"),
("id_whitelist_tip", "Атрымліваць доступ да маёй прылады могуць толькі ID з белага спісу."),
("id_whitelist_wildcard_tip", "Падтрымліваюцца падстаноўныя знакі: '*' адпавядае любой колькасці сімвалаў, '?' — роўна аднаму сімвалу"),
("Invalid ID", "Няправільны ID"),
("Your ID is blocked by the peer", "Ваш ID заблакаваны аддаленай прыладай"),
("Your ip is blocked by the peer", "Ваш IP-адрас заблакаваны аддаленай прыладай"),
("id_whitelist_caveat_tip", "ID паведамляецца кліентам, які падключаецца. Белы спіс памяншае паверхню атакі і не замяняе пароль або 2FA"),
("whitelist_cidr_tip", "Падтрымліваецца натацыя CIDR, напрыклад: 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Релейна връзка"), ("Relay Connection", "Релейна връзка"),
("Secure Connection", "Сигурна връзка"), ("Secure Connection", "Сигурна връзка"),
("Insecure Connection", "Несигурна връзка"), ("Insecure Connection", "Несигурна връзка"),
("Continue", ""),
("Scale original", "Оригинален мащаб"), ("Scale original", "Оригинален мащаб"),
("Scale adaptive", "Приспособимо мащабиране"), ("Scale adaptive", "Приспособимо мащабиране"),
("General", "Основен"), ("General", "Основен"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Превключване на дисплея"), ("Switch display", "Превключване на дисплея"),
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"), ("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"), ("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
("All monitors", "Всички монитори"),
("#{} monitor", "Монитор {}"),
("conn-e2ee-unavailable-tip", "Шифроването от край до край не може да бъде проверено.\nОтдалеченото устройство може все още да се настройва. Опитайте отново по-късно.\nАко това продължи, сървърът може да не е надежден.\nДа се продължи ли въпреки това?"),
("ID whitelisting", "Позволени ID"),
("Use ID whitelisting", "Използване бял списък с ID"),
("id_whitelist_tip", "Само ID от белия списък имат достъп до мен"),
("id_whitelist_wildcard_tip", "Поддържат се заместващи символи: '*' съответства на произволен брой знаци, '?' — на точно един знак"),
("Invalid ID", "Невалидно ID"),
("Your ID is blocked by the peer", "Вашето ID е блокирано от отсрещната страна"),
("Your ip is blocked by the peer", "Вашият IP адрес е блокиран от отсрещната страна"),
("id_whitelist_caveat_tip", "ID се съобщава от свързващия се клиент. Белият списък намалява изложеността и не замества паролата или 2FA"),
("whitelist_cidr_tip", "Поддържа се CIDR нотация, например: 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Connexió amb repetidor"), ("Relay Connection", "Connexió amb repetidor"),
("Secure Connection", "Connexió segura"), ("Secure Connection", "Connexió segura"),
("Insecure Connection", "Connexió no segura"), ("Insecure Connection", "Connexió no segura"),
("Continue", ""),
("Scale original", "Escala original"), ("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptativa"), ("Scale adaptive", "Escala adaptativa"),
("General", "General"), ("General", "General"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Canvia de pantalla"), ("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 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"), ("Show on the minimized toolbar", "Mostra a la barra deines minimitzada"),
("All monitors", "Tots els monitors"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "No s'ha pogut verificar el xifratge d'extrem a extrem.\nEl dispositiu remot encara es pot estar configurant. Torneu-ho a provar més tard.\nSi això continua passant, el servidor pot no ser de confiança.\nVoleu continuar igualment?"),
("ID whitelisting", "ID admesos"),
("Use ID whitelisting", "Utilitza un llistat d'ID admesos"),
("id_whitelist_tip", "Només els ID admesos es podran connectar"),
("id_whitelist_wildcard_tip", "S'admeten comodins: '*' coincideix amb qualsevol nombre de caràcters, '?' amb un sol caràcter"),
("Invalid ID", "ID no vàlid"),
("Your ID is blocked by the peer", "El vostre ID està bloquejat per l'altre extrem"),
("Your ip is blocked by the peer", "La vostra IP està bloquejada per l'altre extrem"),
("id_whitelist_caveat_tip", "L'ID és informat pel client que es connecta. Aquesta llista blanca redueix l'exposició i no substitueix la contrasenya ni la 2FA"),
("whitelist_cidr_tip", "S'admet la notació CIDR, per exemple 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "中继连接"), ("Relay Connection", "中继连接"),
("Secure Connection", "安全连接"), ("Secure Connection", "安全连接"),
("Insecure Connection", "非安全连接"), ("Insecure Connection", "非安全连接"),
("Continue", ""),
("Scale original", "原始尺寸"), ("Scale original", "原始尺寸"),
("Scale adaptive", "适应窗口"), ("Scale adaptive", "适应窗口"),
("General", "常规"), ("General", "常规"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "切换显示器"), ("Switch display", "切换显示器"),
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"), ("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
("Show on the minimized toolbar", "在最小化工具栏上显示"), ("Show on the minimized toolbar", "在最小化工具栏上显示"),
("All monitors", "所有显示器"),
("#{} monitor", "{}号显示器"),
("conn-e2ee-unavailable-tip", "无法验证端到端加密。\n远程设备可能仍在准备中,请稍后重试。\n如果此问题持续出现,服务器可能不受信任。\n仍要继续吗?"),
("ID whitelisting", "ID 白名单"),
("Use ID whitelisting", "只允许白名单上的 ID 访问"),
("id_whitelist_tip", "只有白名单里的 ID 才能访问本机"),
("id_whitelist_wildcard_tip", "支持通配符:'*' 匹配任意数量的字符,'?' 匹配单个字符"),
("Invalid ID", "无效 ID"),
("Your ID is blocked by the peer", "你的 ID 已被对方阻止"),
("Your ip is blocked by the peer", "你的 IP 已被对方阻止"),
("id_whitelist_caveat_tip", "ID 由对端客户端上报,白名单用于减少暴露面,不能替代密码或 2FA"),
("whitelist_cidr_tip", "支持 CIDR 写法,例如 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Připojení předávací server"), ("Relay Connection", "Připojení předávací server"),
("Secure Connection", "Zabezpečené připojení"), ("Secure Connection", "Zabezpečené připojení"),
("Insecure Connection", "Nezabezpečené připojení"), ("Insecure Connection", "Nezabezpečené připojení"),
("Continue", ""),
("Scale original", "Originální měřítko"), ("Scale original", "Originální měřítko"),
("Scale adaptive", "Adaptivní měřítko"), ("Scale adaptive", "Adaptivní měřítko"),
("General", "Obecné"), ("General", "Obecné"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Přepnout obrazovku"), ("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 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ů"), ("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
("All monitors", "Všechny monitory"),
("#{} monitor", "Monitor č. {}"),
("conn-e2ee-unavailable-tip", "Nepodařilo se ověřit koncové šifrování.\nVzdálené zařízení se možná stále nastavuje. Zkuste to znovu později.\nPokud se to bude opakovat, server nemusí být důvěryhodný.\nPřesto pokračovat?"),
("ID whitelisting", "Povolování pouze z daných ID"),
("Use ID whitelisting", "Použít bílou listinu ID"),
("id_whitelist_tip", "Přístup je umožněn pouze z ID, nacházejících se na seznamu povolených"),
("id_whitelist_wildcard_tip", "Jsou podporovány zástupné znaky: '*' odpovídá libovolnému počtu znaků, '?' právě jednomu znaku"),
("Invalid ID", "Neplatné ID"),
("Your ID is blocked by the peer", "Vaše ID je protistranou blokováno"),
("Your ip is blocked by the peer", "Vaše IP adresa je protistranou blokována"),
("id_whitelist_caveat_tip", "ID je hlášeno připojujícím se klientem. Tento seznam snižuje vystavení a nenahrazuje heslo ani 2FA"),
("whitelist_cidr_tip", "Je podporován zápis CIDR, například 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Viderestillingsforbindelse"), ("Relay Connection", "Viderestillingsforbindelse"),
("Secure Connection", "Sikker forbindelse"), ("Secure Connection", "Sikker forbindelse"),
("Insecure Connection", "Usikker forbindelse"), ("Insecure Connection", "Usikker forbindelse"),
("Continue", ""),
("Scale original", "Original skalering"), ("Scale original", "Original skalering"),
("Scale adaptive", "Adaptiv skalering"), ("Scale adaptive", "Adaptiv skalering"),
("General", "Generelt"), ("General", "Generelt"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Skift skærm"), ("Switch display", "Skift skærm"),
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"), ("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"), ("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
("All monitors", "Alle skærme"),
("#{} monitor", "Skærm {}"),
("conn-e2ee-unavailable-tip", "End-to-end-kryptering kunne ikke bekræftes.\nDen eksterne enhed er muligvis stadig ved at blive konfigureret. Prøv igen senere.\nHvis dette fortsætter, er serveren muligvis ikke pålidelig.\nFortsæt alligevel?"),
("ID whitelisting", "ID whitelisting"),
("Use ID whitelisting", "Brug ID whitelisting"),
("id_whitelist_tip", "Kun ID'er på whitelisten kan få adgang til mig"),
("id_whitelist_wildcard_tip", "Jokertegn understøttes: '*' matcher et vilkårligt antal tegn, '?' matcher præcist ét tegn"),
("Invalid ID", "Ugyldigt ID"),
("Your ID is blocked by the peer", "Dit ID er blokeret af modparten"),
("Your ip is blocked by the peer", "Din IP-adresse er blokeret af modparten"),
("id_whitelist_caveat_tip", "ID'et rapporteres af den klient, der opretter forbindelse. Whitelisten reducerer eksponeringen og erstatter ikke adgangskode eller 2FA"),
("whitelist_cidr_tip", "CIDR-notation understøttes, f.eks. 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relay-Verbindung"), ("Relay Connection", "Relay-Verbindung"),
("Secure Connection", "Sichere Verbindung"), ("Secure Connection", "Sichere Verbindung"),
("Insecure Connection", "Unsichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"),
("Continue", ""),
("Scale original", "Keine Skalierung"), ("Scale original", "Keine Skalierung"),
("Scale adaptive", "Anpassbare Skalierung"), ("Scale adaptive", "Anpassbare Skalierung"),
("General", "Allgemein"), ("General", "Allgemein"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Anzeige wechseln"), ("Switch display", "Anzeige wechseln"),
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"), ("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"), ("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
("All monitors", "Alle Bildschirme"),
("#{} monitor", "Bildschirm {}"),
("conn-e2ee-unavailable-tip", "Ende-zu-Ende-Verschlüsselung konnte nicht verifiziert werden.\nDas entfernte Gerät wird möglicherweise noch eingerichtet. Versuchen Sie es später erneut.\nWenn dies weiterhin auftritt, ist der Server möglicherweise nicht vertrauenswürdig.\nTrotzdem fortfahren?"),
("ID whitelisting", "ID-Whitelist"),
("Use ID whitelisting", "ID-Whitelist verwenden"),
("id_whitelist_tip", "Nur IDs auf der Whitelist können zugreifen."),
("id_whitelist_wildcard_tip", "Platzhalter werden unterstützt: '*' steht für beliebig viele Zeichen, '?' für genau ein Zeichen"),
("Invalid ID", "Ungültige ID"),
("Your ID is blocked by the peer", "Ihre ID wird von der Gegenstelle blockiert"),
("Your ip is blocked by the peer", "Ihre IP-Adresse wird von der Gegenstelle blockiert"),
("id_whitelist_caveat_tip", "Die ID wird vom verbindenden Client gemeldet. Die Whitelist verringert die Angriffsfläche und ersetzt weder Passwort noch 2FA"),
("whitelist_cidr_tip", "Die CIDR-Notation wird unterstützt, z. B. 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Αναμεταδιδόμενη σύνδεση"), ("Relay Connection", "Αναμεταδιδόμενη σύνδεση"),
("Secure Connection", "Ασφαλής σύνδεση"), ("Secure Connection", "Ασφαλής σύνδεση"),
("Insecure Connection", "Μη ασφαλής σύνδεση"), ("Insecure Connection", "Μη ασφαλής σύνδεση"),
("Continue", ""),
("Scale original", "Κλιμάκωση πρωτότυπου"), ("Scale original", "Κλιμάκωση πρωτότυπου"),
("Scale adaptive", "Προσαρμοσμένη κλίμακα"), ("Scale adaptive", "Προσαρμοσμένη κλίμακα"),
("General", "Γενικά"), ("General", "Γενικά"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Εναλλαγή οθόνης"), ("Switch display", "Εναλλαγή οθόνης"),
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"), ("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"), ("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
("All monitors", "Όλες οι οθόνες"),
("#{} monitor", "Οθόνη {}"),
("conn-e2ee-unavailable-tip", "Δεν ήταν δυνατή η επαλήθευση της κρυπτογράφησης από άκρο σε άκρο.\nΗ απομακρυσμένη συσκευή μπορεί να ρυθμίζεται ακόμα. Δοκιμάστε ξανά αργότερα.\nΑν αυτό συνεχιστεί, ο διακομιστής μπορεί να μην είναι αξιόπιστος.\nΣυνέχεια παρ' όλα αυτά;"),
("ID whitelisting", "Λίστα επιτρεπόμενων ID"),
("Use ID whitelisting", "Χρήση λίστας επιτρεπόμενων ID"),
("id_whitelist_tip", "Μόνο τα ID της λίστας επιτρεπόμενων έχουν πρόσβαση σε εμένα"),
("id_whitelist_wildcard_tip", "Υποστηρίζονται χαρακτήρες μπαλαντέρ: το '*' αντιστοιχεί σε οποιονδήποτε αριθμό χαρακτήρων, το '?' σε έναν ακριβώς χαρακτήρα"),
("Invalid ID", "Μη έγκυρο ID"),
("Your ID is blocked by the peer", "Το ID σας έχει αποκλειστεί από τον απομακρυσμένο υπολογιστή"),
("Your ip is blocked by the peer", "Η διεύθυνση IP σας έχει αποκλειστεί από τον απομακρυσμένο υπολογιστή"),
("id_whitelist_caveat_tip", "Το ID αναφέρεται από τον πελάτη που συνδέεται. Η λίστα επιτρεπόμενων μειώνει την έκθεση και δεν αντικαθιστά τον κωδικό πρόσβασης ή το 2FA"),
("whitelist_cidr_tip", "Υποστηρίζεται η σημειογραφία CIDR, π.χ. 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+6
View File
@@ -279,5 +279,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland-soft-keyboard-input-label", "Soft keyboard input"), ("wayland-soft-keyboard-input-label", "Soft keyboard input"),
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"), ("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"), ("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
("conn-e2ee-unavailable-tip", "Could not verify end-to-end encryption.\nThe remote device may still be setting up. Try again later.\nIf this keeps happening, the server may be untrusted.\nContinue anyway?"),
("id_whitelist_tip", "Only whitelisted IDs can access me"),
("id_whitelist_wildcard_tip", "Wildcards are supported: '*' matches any number of characters, '?' matches exactly one character"),
("id_whitelist_caveat_tip", "The ID is reported by the connecting client. This whitelist reduces exposure and does not replace the password or 2FA."),
("whitelist_cidr_tip", "CIDR notation is supported, e.g. 192.168.1.0/24"),
("Your ip is blocked by the peer", "Your IP is blocked by the peer")
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relajsa Konekto"), ("Relay Connection", "Relajsa Konekto"),
("Secure Connection", "Sekura Konekto"), ("Secure Connection", "Sekura Konekto"),
("Insecure Connection", "Nesekura Konekto"), ("Insecure Connection", "Nesekura Konekto"),
("Continue", ""),
("Scale original", "Skalo originalo"), ("Scale original", "Skalo originalo"),
("Scale adaptive", "Skalo adapta"), ("Scale adaptive", "Skalo adapta"),
("General", "Ĝenerala"), ("General", "Ĝenerala"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ŝalti ekranon"), ("Switch display", "Ŝalti ekranon"),
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"), ("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"), ("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
("All monitors", "Ĉiuj monitoroj"),
("#{} monitor", "Monitoro {}"),
("conn-e2ee-unavailable-tip", "Ne eblis kontroli la fin-al-finan ĉifradon.\nLa fora aparato eble ankoraŭ estas agordata. Provu denove poste.\nSe tio daŭre okazas, la servilo eble estas nefidinda.\nĈu daŭrigi tamen?"),
("ID whitelisting", "Listo de ID akceptataj"),
("Use ID whitelisting", "Uzi liston de ID akceptataj"),
("id_whitelist_tip", "Nur la ID en la blanka listo povas kontroli mian komputilon"),
("id_whitelist_wildcard_tip", "Ĵokeroj estas subtenataj: '*' kongruas kun iu ajn nombro da signoj, '?' kun ekzakte unu signo"),
("Invalid ID", "ID nevalida"),
("Your ID is blocked by the peer", "Via ID estas blokita de la alia flanko"),
("Your ip is blocked by the peer", "Via IP estas blokita de la alia flanko"),
("id_whitelist_caveat_tip", "La ID estas raportata de la konektiĝanta kliento. La blanka listo malpliigas la eksponiĝon kaj ne anstataŭas la pasvorton aŭ 2FA"),
("whitelist_cidr_tip", "La notacio CIDR estas subtenata, ekzemple 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexión Relay"), ("Relay Connection", "Conexión Relay"),
("Secure Connection", "Conexión segura"), ("Secure Connection", "Conexión segura"),
("Insecure Connection", "Conexión insegura"), ("Insecure Connection", "Conexión insegura"),
("Continue", ""),
("Scale original", "Escala original"), ("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptativa"), ("Scale adaptive", "Escala adaptativa"),
("General", "General"), ("General", "General"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Cambiar de pantalla"), ("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 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"), ("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
("All monitors", "Todos los monitores"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "No se pudo verificar el cifrado de extremo a extremo.\nEs posible que el dispositivo remoto aún se esté configurando. Inténtelo de nuevo más tarde.\nSi esto sigue ocurriendo, es posible que el servidor no sea de confianza.\n¿Continuar de todos modos?"),
("ID whitelisting", "IDs admitidos"),
("Use ID whitelisting", "Usar lista de IDs admitidos"),
("id_whitelist_tip", "Solo los IDs autorizados pueden conectarse a este escritorio"),
("id_whitelist_wildcard_tip", "Se admiten comodines: '*' coincide con cualquier número de caracteres, '?' con exactamente un carácter"),
("Invalid ID", "ID incorrecto"),
("Your ID is blocked by the peer", "Tu ID está bloqueado por el dispositivo remoto"),
("Your ip is blocked by the peer", "Tu IP está bloqueada por el dispositivo remoto"),
("id_whitelist_caveat_tip", "El ID lo comunica el cliente que se conecta. Esta lista blanca reduce la exposición y no sustituye a la contraseña ni al 2FA"),
("whitelist_cidr_tip", "Se admite la notación CIDR, por ejemplo 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Releeühendus"), ("Relay Connection", "Releeühendus"),
("Secure Connection", "Turvaline ühendus"), ("Secure Connection", "Turvaline ühendus"),
("Insecure Connection", "Ebaturvaline ühendus"), ("Insecure Connection", "Ebaturvaline ühendus"),
("Continue", ""),
("Scale original", "Originaalskaala"), ("Scale original", "Originaalskaala"),
("Scale adaptive", "Kohanduv skaala"), ("Scale adaptive", "Kohanduv skaala"),
("General", "Üldine"), ("General", "Üldine"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Vaheta kuva"), ("Switch display", "Vaheta kuva"),
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"), ("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"), ("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
("All monitors", "Kõik kuvarid"),
("#{} monitor", "Kuvar {}"),
("conn-e2ee-unavailable-tip", "Otspunktkrüptimist ei saanud kontrollida.\nKaugseade võib olla veel seadistamisel. Proovige hiljem uuesti.\nKui see jätkub, ei pruugi server olla usaldusväärne.\nKas jätkata siiski?"),
("ID whitelisting", "ID lubamisloend"),
("Use ID whitelisting", "Kasuta ID-lubamisloendit"),
("id_whitelist_tip", "Ainult lubamisloendis ID saab mulle ligi"),
("id_whitelist_wildcard_tip", "Metamärgid on toetatud: '*' vastab suvalisele arvule märkidele, '?' täpselt ühele märgile"),
("Invalid ID", "Sobimatu ID"),
("Your ID is blocked by the peer", "Teine pool on sinu ID blokeerinud"),
("Your ip is blocked by the peer", "Teine pool on sinu IP-aadressi blokeerinud"),
("id_whitelist_caveat_tip", "ID edastab ühenduv klient. Lubamisloend vähendab eksponeeritust ega asenda parooli või 2FA-d"),
("whitelist_cidr_tip", "Toetatud on CIDR-tähistus, näiteks 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Konexio igorria"), ("Relay Connection", "Konexio igorria"),
("Secure Connection", "Konexio segurua"), ("Secure Connection", "Konexio segurua"),
("Insecure Connection", "Konexio ez-segurua"), ("Insecure Connection", "Konexio ez-segurua"),
("Continue", ""),
("Scale original", "Jatorrizko eskala"), ("Scale original", "Jatorrizko eskala"),
("Scale adaptive", "Eskala moldagarria"), ("Scale adaptive", "Eskala moldagarria"),
("General", "Orokorra"), ("General", "Orokorra"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Aldatu pantaila"), ("Switch display", "Aldatu pantaila"),
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"), ("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"), ("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
("All monitors", "Monitore guztiak"),
("#{} monitor", "{}. monitorea"),
("conn-e2ee-unavailable-tip", "Ezin izan da muturretik muturrerako enkriptatzea egiaztatu.\nUrruneko gailua oraindik konfiguratzen ari daiteke. Saiatu berriro geroago.\nHonek jarraitzen badu, zerbitzaria fidagaitza izan daiteke.\nHala ere jarraitu?"),
("ID whitelisting", "Onartutako IDak"),
("Use ID whitelisting", "Erabili ID onartuen zerrenda"),
("id_whitelist_tip", "Baimendutako IDak soilik konektatu daitezke mahaigain honetara"),
("id_whitelist_wildcard_tip", "Komodinak onartzen dira: '*' edozein karaktere kopururekin bat dator, '?' karaktere bakar batekin"),
("Invalid ID", "ID baliogabea"),
("Your ID is blocked by the peer", "Beste aldeak zure IDa blokeatu du"),
("Your ip is blocked by the peer", "Beste aldeak zure IP helbidea blokeatu du"),
("id_whitelist_caveat_tip", "IDa konektatzen den bezeroak jakinarazten du. Zerrenda honek esposizioa murrizten du eta ez du pasahitza edo 2FA ordezkatzen"),
("whitelist_cidr_tip", "CIDR notazioa onartzen da, adibidez 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relay ارتباط"), ("Relay Connection", "Relay ارتباط"),
("Secure Connection", "ارتباط امن"), ("Secure Connection", "ارتباط امن"),
("Insecure Connection", "ارتباط غیر امن"), ("Insecure Connection", "ارتباط غیر امن"),
("Continue", ""),
("Scale original", "مقیاس اصلی"), ("Scale original", "مقیاس اصلی"),
("Scale adaptive", "مقیاس تطبیقی"), ("Scale adaptive", "مقیاس تطبیقی"),
("General", "عمومی"), ("General", "عمومی"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "تعویض نمایشگر"), ("Switch display", "تعویض نمایشگر"),
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"), ("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچک‌شده"), ("Show on the minimized toolbar", "نمایش در نوار ابزار کوچک‌شده"),
("All monitors", "همه نمایشگرها"),
("#{} monitor", "نمایشگر {}"),
("conn-e2ee-unavailable-tip", "رمزنگاری سرتاسری قابل تأیید نیست.\nدستگاه راه دور ممکن است هنوز در حال آماده‌سازی باشد. بعداً دوباره تلاش کنید.\nاگر این مشکل ادامه داشت، سرور ممکن است نامطمئن باشد.\nبا این حال ادامه می‌دهید؟"),
("ID whitelisting", "لیست شناسه های مجاز"),
("Use ID whitelisting", "استفاده از لیست شناسه های مجاز"),
("id_whitelist_tip", "فقط شناسه های مجاز می توانند به این دسکتاپ متصل شوند"),
("id_whitelist_wildcard_tip", "نویسه های عام پشتیبانی می شوند: '*' با هر تعداد نویسه و '?' دقیقا با یک نویسه مطابقت دارد"),
("Invalid ID", "شناسه نامعتبر است"),
("Your ID is blocked by the peer", "شناسه شما توسط طرف مقابل مسدود شده است"),
("Your ip is blocked by the peer", "نشانی IP شما توسط طرف مقابل مسدود شده است"),
("id_whitelist_caveat_tip", "شناسه توسط کلاینت متصل شونده گزارش می شود. لیست مجاز سطح در معرض بودن را کاهش می دهد و جایگزین رمز عبور یا 2FA نیست"),
("whitelist_cidr_tip", "نماد CIDR پشتیبانی می شود، برای مثال 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Välitetty yhteys"), ("Relay Connection", "Välitetty yhteys"),
("Secure Connection", "Suojattu yhteys"), ("Secure Connection", "Suojattu yhteys"),
("Insecure Connection", "Suojaamaton yhteys"), ("Insecure Connection", "Suojaamaton yhteys"),
("Continue", ""),
("Scale original", "Skaalaa alkuperäinen"), ("Scale original", "Skaalaa alkuperäinen"),
("Scale adaptive", "Mukautuva skaalaus"), ("Scale adaptive", "Mukautuva skaalaus"),
("General", "Yleiset"), ("General", "Yleiset"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Vaihda näyttöä"), ("Switch display", "Vaihda näyttöä"),
("Show monitor switch button on the main toolbar", "Näytä näytön vaihtopainike päätyökalurivillä"), ("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ä"), ("Show on the minimized toolbar", "Näytä pienennetyssä työkalurivissä"),
("All monitors", "Kaikki näytöt"),
("#{} monitor", "Näyttö {}"),
("conn-e2ee-unavailable-tip", "Päästä päähän -salausta ei voitu vahvistaa.\nEtälaite voi olla vielä määritettävänä. Yritä myöhemmin uudelleen.\nJos tämä jatkuu, palvelin ei ehkä ole luotettava.\nJatketaanko silti?"),
("ID whitelisting", "ID sallintalista"),
("Use ID whitelisting", "Käytä ID sallintalistaa"),
("id_whitelist_tip", "Vain sallitut ID:t voivat muodostaa yhteyden"),
("id_whitelist_wildcard_tip", "Jokerimerkit ovat tuettuja: '*' vastaa mitä tahansa määrää merkkejä, '?' täsmälleen yhtä merkkiä"),
("Invalid ID", "Virheellinen ID"),
("Your ID is blocked by the peer", "Vastapuoli on estänyt ID:si"),
("Your ip is blocked by the peer", "Vastapuoli on estänyt IP-osoitteesi"),
("id_whitelist_caveat_tip", "ID on yhdistävän asiakkaan ilmoittama. Sallintalista pienentää altistusta eikä korvaa salasanaa tai 2FA:ta"),
("whitelist_cidr_tip", "CIDR-merkintä on tuettu, esimerkiksi 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Connexion via relais"), ("Relay Connection", "Connexion via relais"),
("Secure Connection", "Connexion sécurisée"), ("Secure Connection", "Connexion sécurisée"),
("Insecure Connection", "Connexion non sécurisée"), ("Insecure Connection", "Connexion non sécurisée"),
("Continue", ""),
("Scale original", "Échelle originale"), ("Scale original", "Échelle originale"),
("Scale adaptive", "Échelle adaptative"), ("Scale adaptive", "Échelle adaptative"),
("General", "Général"), ("General", "Général"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Changer d’écran"), ("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 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"), ("Show on the minimized toolbar", "Afficher dans la barre doutils réduite"),
("All monitors", "Tous les moniteurs"),
("#{} monitor", "Moniteur {}"),
("conn-e2ee-unavailable-tip", "Impossible de vérifier le chiffrement de bout en bout.\nL'appareil distant est peut-être encore en cours de configuration. Réessayez plus tard.\nSi le problème persiste, le serveur n'est peut-être pas fiable.\nContinuer quand même ?"),
("ID whitelisting", "Liste blanche dID"),
("Use ID whitelisting", "Utiliser une liste blanche dID"),
("id_whitelist_tip", "Seuls les ID inclus dans la liste blanche pourront accéder à mon appareil"),
("id_whitelist_wildcard_tip", "Les caractères génériques sont pris en charge : '*' correspond à un nombre quelconque de caractères, '?' à un seul caractère"),
("Invalid ID", "ID non valide"),
("Your ID is blocked by the peer", "Votre ID est bloqué par lappareil distant"),
("Your ip is blocked by the peer", "Votre adresse IP est bloquée par lappareil distant"),
("id_whitelist_caveat_tip", "LID est déclaré par le client qui se connecte. Cette liste blanche réduit lexposition et ne remplace ni le mot de passe ni la 2FA"),
("whitelist_cidr_tip", "La notation CIDR est prise en charge, par exemple 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "რეტრანსლირებული კავშირი"), ("Relay Connection", "რეტრანსლირებული კავშირი"),
("Secure Connection", "უსაფრთხო კავშირი"), ("Secure Connection", "უსაფრთხო კავშირი"),
("Insecure Connection", "არაუსაფრთხო კავშირი"), ("Insecure Connection", "არაუსაფრთხო კავშირი"),
("Continue", ""),
("Scale original", "ორიგინალური მასშტაბი"), ("Scale original", "ორიგინალური მასშტაბი"),
("Scale adaptive", "ადაპტირებადი მასშტაბი"), ("Scale adaptive", "ადაპტირებადი მასშტაბი"),
("General", "ზოგადი"), ("General", "ზოგადი"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ეკრანის გადართვა"), ("Switch display", "ეკრანის გადართვა"),
("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"), ("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"),
("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"), ("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"),
("All monitors", "ყველა მონიტორი"),
("#{} monitor", "მონიტორი {}"),
("conn-e2ee-unavailable-tip", "ბოლომდე დაშიფვრის გადამოწმება ვერ მოხერხდა.\nდისტანციური მოწყობილობა შესაძლოა ჯერ კიდევ მზადდება. სცადეთ მოგვიანებით.\nთუ ეს კვლავ გაგრძელდება, სერვერი შესაძლოა არასანდო იყოს.\nმაინც გააგრძელებთ?"),
("ID whitelisting", "დაშვებული ID-ების სია"),
("Use ID whitelisting", "ID თეთრი სიის გამოყენება"),
("id_whitelist_tip", "მხოლოდ თეთრ სიაში არსებულ ID-ებს შეუძლიათ ჩემს მოწყობილობაზე წვდომა."),
("id_whitelist_wildcard_tip", "მხარდაჭერილია ვაილდქარდები: '*' ემთხვევა ნებისმიერი რაოდენობის სიმბოლოს, '?' — ზუსტად ერთ სიმბოლოს"),
("Invalid ID", "არასწორი ID"),
("Your ID is blocked by the peer", "თქვენი ID დაბლოკილია მეორე მხარის მიერ"),
("Your ip is blocked by the peer", "თქვენი IP მისამართი დაბლოკილია მეორე მხარის მიერ"),
("id_whitelist_caveat_tip", "ID-ს აცხადებს დამაკავშირებელი კლიენტი. თეთრი სია ამცირებს ექსპოზიციას და ვერ ჩაანაცვლებს პაროლს ან 2FA-ს"),
("whitelist_cidr_tip", "მხარდაჭერილია CIDR ჩანაწერი, მაგალითად 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "રિલે કનેક્શન"), ("Relay Connection", "રિલે કનેક્શન"),
("Secure Connection", "સુરક્ષિત કનેક્શન"), ("Secure Connection", "સુરક્ષિત કનેક્શન"),
("Insecure Connection", "અસુરક્ષિત કનેક્શન"), ("Insecure Connection", "અસુરક્ષિત કનેક્શન"),
("Continue", ""),
("Scale original", "મૂળ સ્કેલ"), ("Scale original", "મૂળ સ્કેલ"),
("Scale adaptive", "એડેપ્ટિવ સ્કેલ"), ("Scale adaptive", "એડેપ્ટિવ સ્કેલ"),
("General", "સામાન્ય"), ("General", "સામાન્ય"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ડિસ્પ્લે બદલો"), ("Switch display", "ડિસ્પ્લે બદલો"),
("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"), ("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"),
("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"), ("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"),
("All monitors", "બધા મોનિટર"),
("#{} monitor", "મોનિટર {}"),
("conn-e2ee-unavailable-tip", "એન્ડ-ટુ-એન્ડ એન્ક્રિપ્શન ચકાસી શકાયું નથી.\nરિમોટ ઉપકરણ હજી સેટ થઈ રહ્યું હોઈ શકે છે. પછીથી ફરી પ્રયાસ કરો.\nજો આ ચાલુ રહે, તો સર્વર અવિશ્વસનીય હોઈ શકે છે.\nશું તેમ છતાં ચાલુ રાખવું?"),
("ID whitelisting", "ID વ્હાઇટલિસ્ટિંગ"),
("Use ID whitelisting", "ID વ્હાઇટલિસ્ટિંગનો ઉપયોગ કરો"),
("id_whitelist_tip", "માત્ર વ્હાઇટલિસ્ટ કરેલ ID જ મને એક્સેસ કરી શકે છે"),
("id_whitelist_wildcard_tip", "વાઇલ્ડકાર્ડ સપોર્ટેડ છે: '*' કોઈપણ સંખ્યાના અક્ષરો સાથે મેળ ખાય છે, '?' બરાબર એક અક્ષર સાથે"),
("Invalid ID", "અમાન્ય ID"),
("Your ID is blocked by the peer", "તમારું ID સામેના પક્ષ દ્વારા બ્લોક કરવામાં આવ્યું છે"),
("Your ip is blocked by the peer", "તમારું IP સામેના પક્ષ દ્વારા બ્લોક કરવામાં આવ્યું છે"),
("id_whitelist_caveat_tip", "ID કનેક્ટ થતા ક્લાયન્ટ દ્વારા જણાવવામાં આવે છે. વ્હાઇટલિસ્ટ એક્સપોઝર ઘટાડે છે અને પાસવર્ડ કે 2FA નો વિકલ્પ નથી"),
("whitelist_cidr_tip", "CIDR નોટેશન સપોર્ટેડ છે, ઉदાહરણ તરીકે 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "חיבור באמצעות ממסר"), ("Relay Connection", "חיבור באמצעות ממסר"),
("Secure Connection", "חיבור מאובטח"), ("Secure Connection", "חיבור מאובטח"),
("Insecure Connection", "חיבור לא מאובטח"), ("Insecure Connection", "חיבור לא מאובטח"),
("Continue", ""),
("Scale original", "קנה מידה מקורי"), ("Scale original", "קנה מידה מקורי"),
("Scale adaptive", "קנה מידה מותאם"), ("Scale adaptive", "קנה מידה מותאם"),
("General", "כללי"), ("General", "כללי"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "החלפת צג"), ("Switch display", "החלפת צג"),
("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"), ("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"),
("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"), ("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"),
("All monitors", "כל המסכים"),
("#{} monitor", "מסך {}"),
("conn-e2ee-unavailable-tip", "לא ניתן לאמת הצפנה מקצה לקצה.\nייתכן שהמכשיר המרוחק עדיין בתהליך הגדרה. נסה שוב מאוחר יותר.\nאם זה ממשיך לקרות, ייתכן שהשרת אינו מהימן.\nלהמשיך בכל זאת?"),
("ID whitelisting", "רשימת מזהים מורשים"),
("Use ID whitelisting", "השתמש ברשימה לבנה של מזהים"),
("id_whitelist_tip", "רק מזהים מהרשימה הלבנה יכולים לגשת אלי"),
("id_whitelist_wildcard_tip", "נתמכים תווים כלליים: '*' תואם כל מספר של תווים, '?' תואם תו אחד בדיוק"),
("Invalid ID", "מזהה לא תקין"),
("Your ID is blocked by the peer", "המזהה שלך נחסם על ידי הצד המרוחק"),
("Your ip is blocked by the peer", "כתובת ה-IP שלך נחסמה על ידי הצד המרוחק"),
("id_whitelist_caveat_tip", "המזהה מדווח על ידי הלקוח המתחבר. הרשימה הלבנה מצמצמת חשיפה ואינה מחליפה סיסמה או 2FA"),
("whitelist_cidr_tip", "יש תמיכה בסימון CIDR, לדוגמה 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "रिले कनेक्शन"), ("Relay Connection", "रिले कनेक्शन"),
("Secure Connection", "सुरक्षित कनेक्शन"), ("Secure Connection", "सुरक्षित कनेक्शन"),
("Insecure Connection", "असुरक्षित कनेक्शन"), ("Insecure Connection", "असुरक्षित कनेक्शन"),
("Continue", ""),
("Scale original", "मूल पैमाना"), ("Scale original", "मूल पैमाना"),
("Scale adaptive", "अनुकूली पैमाना"), ("Scale adaptive", "अनुकूली पैमाना"),
("General", "सामान्य"), ("General", "सामान्य"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "डिस्प्ले बदलें"), ("Switch display", "डिस्प्ले बदलें"),
("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"), ("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"),
("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"), ("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"),
("All monitors", "सभी मॉनिटर"),
("#{} monitor", "मॉनिटर {}"),
("conn-e2ee-unavailable-tip", "एंड-टू-एंड एन्क्रिप्शन सत्यापित नहीं किया जा सका।\nदूरस्थ डिवाइस अभी भी सेट अप हो रहा हो सकता है। बाद में फिर प्रयास करें।\nयदि यह समस्या बनी रहती है, तो सर्वर अविश्वसनीय हो सकता है।\nफिर भी जारी रखें?"),
("ID whitelisting", "ID श्वेतसूची (Whitelisting)"),
("Use ID whitelisting", "ID श्वेतसूची का उपयोग करें"),
("id_whitelist_tip", "केवल श्वेतसूचीबद्ध ID ही मुझ तक पहुंच सकते हैं"),
("id_whitelist_wildcard_tip", "वाइल्डकार्ड समर्थित हैं: '*' किसी भी संख्या के अक्षरों से मेल खाता है, '?' ठीक एक अक्षर से"),
("Invalid ID", "अमान्य ID"),
("Your ID is blocked by the peer", "आपकी ID दूसरे पक्ष द्वारा अवरुद्ध कर दी गई है"),
("Your ip is blocked by the peer", "आपका IP दूसरे पक्ष द्वारा अवरुद्ध कर दिया गया है"),
("id_whitelist_caveat_tip", "ID कनेक्ट करने वाले क्लाइंट द्वारा बताई जाती है। श्वेतसूची जोखिम कम करती है और पासवर्ड या 2FA का विकल्प नहीं है"),
("whitelist_cidr_tip", "CIDR नोटेशन समर्थित है, उदाहरण के लिए 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Posredna veza"), ("Relay Connection", "Posredna veza"),
("Secure Connection", "Sigurna veza"), ("Secure Connection", "Sigurna veza"),
("Insecure Connection", "Nesigurna veza"), ("Insecure Connection", "Nesigurna veza"),
("Continue", ""),
("Scale original", "Skaliraj izvornik"), ("Scale original", "Skaliraj izvornik"),
("Scale adaptive", "Prilagođeno skaliranje"), ("Scale adaptive", "Prilagođeno skaliranje"),
("General", "Općenito"), ("General", "Općenito"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Promijeni zaslon"), ("Switch display", "Promijeni zaslon"),
("Show monitor switch button on the main toolbar", "Prikaži gumb za prebacivanje monitora na glavnoj alatnoj traci"), ("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"), ("Show on the minimized toolbar", "Prikaži na minimiziranoj alatnoj traci"),
("All monitors", "Svi monitori"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "End-to-end enkripcija nije mogla biti potvrđena.\nUdaljeni uređaj se možda još postavlja. Pokušajte ponovno kasnije.\nAko se to nastavi događati, poslužitelj možda nije pouzdan.\nIpak nastaviti?"),
("ID whitelisting", "ID pouzdana lista"),
("Use ID whitelisting", "Koristi popis pouzdanih ID-ova"),
("id_whitelist_tip", "Mogu mi pristupiti samo dozvoljeni ID-ovi"),
("id_whitelist_wildcard_tip", "Podržani su zamjenski znakovi: '*' odgovara bilo kojem broju znakova, '?' točno jednom znaku"),
("Invalid ID", "Nevažeći ID"),
("Your ID is blocked by the peer", "Vaš ID je blokiralo udaljeno računalo"),
("Your ip is blocked by the peer", "Vašu IP adresu je blokiralo udaljeno računalo"),
("id_whitelist_caveat_tip", "ID prijavljuje klijent koji se povezuje. Ova lista smanjuje izloženost i ne zamjenjuje lozinku ni 2FA"),
("whitelist_cidr_tip", "Podržan je CIDR zapis, primjerice 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"), ("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"),
("Secure Connection", "Biztonságos kapcsolat"), ("Secure Connection", "Biztonságos kapcsolat"),
("Insecure Connection", "Nem biztonságos kapcsolat"), ("Insecure Connection", "Nem biztonságos kapcsolat"),
("Continue", ""),
("Scale original", "Eredeti méretarány"), ("Scale original", "Eredeti méretarány"),
("Scale adaptive", "Adaptív méretarány"), ("Scale adaptive", "Adaptív méretarány"),
("General", "Általános"), ("General", "Általános"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Kijelző váltása"), ("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 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"), ("Show on the minimized toolbar", "Megjelenítés a kis méretű eszköztáron"),
("All monitors", "Minden monitor"),
("#{} monitor", "{}. monitor"),
("conn-e2ee-unavailable-tip", "A végpontok közötti titkosítás nem volt ellenőrizhető.\nA távoli eszköz talán még beállítás alatt áll. Próbálja újra később.\nHa ez továbbra is előfordul, a szerver lehet, hogy nem megbízható.\nFolytatja így is?"),
("ID whitelisting", "Azonosító engedélyezési lista"),
("Use ID whitelisting", "Azonosító engedélyezési lista használata"),
("id_whitelist_tip", "Csak az engedélyezési listán szereplő azonosítók kapcsolódhatnak"),
("id_whitelist_wildcard_tip", "Helyettesítő karakterek használhatók: a '*' tetszőleges számú karakternek, a '?' pontosan egy karakternek felel meg"),
("Invalid ID", "Érvénytelen azonosító"),
("Your ID is blocked by the peer", "Az azonosítóját a távoli fél letiltotta"),
("Your ip is blocked by the peer", "Az IP-címét a távoli fél letiltotta"),
("id_whitelist_caveat_tip", "Az azonosítót a csatlakozó kliens jelenti. Az engedélyezési lista csökkenti a kitettséget, és nem helyettesíti a jelszót vagy a 2FA-t"),
("whitelist_cidr_tip", "A CIDR jelölés támogatott, például 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Koneksi Relay"), ("Relay Connection", "Koneksi Relay"),
("Secure Connection", "Koneksi aman"), ("Secure Connection", "Koneksi aman"),
("Insecure Connection", "Koneksi Tidak Aman"), ("Insecure Connection", "Koneksi Tidak Aman"),
("Continue", ""),
("Scale original", "Skala asli"), ("Scale original", "Skala asli"),
("Scale adaptive", "Skala adaptif"), ("Scale adaptive", "Skala adaptif"),
("General", "Umum"), ("General", "Umum"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ganti tampilan"), ("Switch display", "Ganti tampilan"),
("Show monitor switch button on the main toolbar", "Tampilkan tombol pengalih monitor di bilah alat utama"), ("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"), ("Show on the minimized toolbar", "Tampilkan di bilah alat yang diperkecil"),
("All monitors", "Semua monitor"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "Tidak dapat memverifikasi enkripsi ujung ke ujung.\nPerangkat jarak jauh mungkin masih disiapkan. Coba lagi nanti.\nJika ini terus terjadi, server mungkin tidak tepercaya.\nTetap lanjutkan?"),
("ID whitelisting", "Daftar ID yang diizinkan"),
("Use ID whitelisting", "Gunakan daftar ID yang diizinkan"),
("id_whitelist_tip", "Hanya ID yang diizinkan dapat mengakses"),
("id_whitelist_wildcard_tip", "Wildcard didukung: '*' cocok dengan berapa pun jumlah karakter, '?' dengan tepat satu karakter"),
("Invalid ID", "ID tidak valid"),
("Your ID is blocked by the peer", "ID Anda diblokir oleh perangkat remote"),
("Your ip is blocked by the peer", "IP Anda diblokir oleh perangkat remote"),
("id_whitelist_caveat_tip", "ID dilaporkan oleh klien yang terhubung. Daftar ini mengurangi paparan dan bukan pengganti kata sandi atau 2FA"),
("whitelist_cidr_tip", "Notasi CIDR didukung, misalnya 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Connessione relay"), ("Relay Connection", "Connessione relay"),
("Secure Connection", "Connessione sicura"), ("Secure Connection", "Connessione sicura"),
("Insecure Connection", "Connessione non sicura"), ("Insecure Connection", "Connessione non sicura"),
("Continue", "Continua"),
("Scale original", "Scala originale"), ("Scale original", "Scala originale"),
("Scale adaptive", "Scala adattiva"), ("Scale adaptive", "Scala adattiva"),
("General", "Generale"), ("General", "Generale"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Cambia schermo"), ("Switch display", "Cambia schermo"),
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio 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"), ("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
("All monitors", "Tutti gli schermi"),
("#{} monitor", "Schermo {}"),
("conn-e2ee-unavailable-tip", "Impossibile verificare la crittografia end-to-end.\nIl dispositivo remoto potrebbe essere ancora in configurazione. Riprova più tardi.\nSe il problema persiste, il server potrebbe non essere attendibile.\nVuoi continuare?"),
("ID whitelisting", "ID autorizzati"),
("Use ID whitelisting", "Usa elenco ID autorizzati"),
("id_whitelist_tip", "Possono connettersi a questo desktop solo gli ID autorizzati"),
("id_whitelist_wildcard_tip", "Sono supportati i caratteri jolly: '*' corrisponde a un numero qualsiasi di caratteri, '?' a un solo carattere"),
("Invalid ID", "ID non valido"),
("Your ID is blocked by the peer", "Il tuo ID è bloccato dal dispositivo remoto"),
("Your ip is blocked by the peer", "Il tuo IP è bloccato dal dispositivo remoto"),
("id_whitelist_caveat_tip", "L'ID è dichiarato dal client che si connette. Questo elenco riduce l'esposizione e non sostituisce la password o la 2FA"),
("whitelist_cidr_tip", "È supportata la notazione CIDR, ad esempio 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "中継接続"), ("Relay Connection", "中継接続"),
("Secure Connection", "安全な接続"), ("Secure Connection", "安全な接続"),
("Insecure Connection", "安全でない接続"), ("Insecure Connection", "安全でない接続"),
("Continue", ""),
("Scale original", "オリジナルのサイズ"), ("Scale original", "オリジナルのサイズ"),
("Scale adaptive", "ウィンドウに合わせる"), ("Scale adaptive", "ウィンドウに合わせる"),
("General", "一般"), ("General", "一般"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ディスプレイを切り替え"), ("Switch display", "ディスプレイを切り替え"),
("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"), ("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"),
("Show on the minimized toolbar", "最小化したツールバーに表示"), ("Show on the minimized toolbar", "最小化したツールバーに表示"),
("All monitors", "すべてのディスプレイ"),
("#{} monitor", "ディスプレイ {}"),
("conn-e2ee-unavailable-tip", "エンドツーエンド暗号化を確認できませんでした。\nリモートデバイスはまだ準備中の可能性があります。後でもう一度お試しください。\nこの問題が続く場合、サーバーが信頼できない可能性があります。\nそれでも続行しますか?"),
("ID whitelisting", "ID ホワイトリスト"),
("Use ID whitelisting", "ID ホワイトリストを使用する"),
("id_whitelist_tip", "ホワイトリストに登録された ID からのみ接続を許可します"),
("id_whitelist_wildcard_tip", "ワイルドカードが使用できます。'*' は任意の数の文字、'?' はちょうど1文字に一致します"),
("Invalid ID", "無効な ID"),
("Your ID is blocked by the peer", "あなたの ID は接続先によってブロックされています"),
("Your ip is blocked by the peer", "あなたの IP アドレスは接続先によってブロックされています"),
("id_whitelist_caveat_tip", "ID は接続するクライアントから申告されます。ホワイトリストは露出を減らすもので、パスワードや 2FA の代わりにはなりません"),
("whitelist_cidr_tip", "CIDR 表記に対応しています。例: 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+18 -6
View File
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "화면 공유"), ("Screen Share", "화면 공유"),
("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), ("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."),
("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), ("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", ""),
("JumpLink", "점프 링크"), ("JumpLink", "점프 링크"),
("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"),
("Show RustDesk", "RustDesk 표시"), ("Show RustDesk", "RustDesk 표시"),
@@ -749,17 +749,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Deploy", "배포"), ("Deploy", "배포"),
("Custom ID (optional)", "사용자 지정 ID (선택 사항)"), ("Custom ID (optional)", "사용자 지정 ID (선택 사항)"),
("server_requires_deployment_tip", "서버에서 이 장치를 명시적으로 배포하도록 요구합니다. 지금 배포하시겠습니까?"), ("server_requires_deployment_tip", "서버에서 이 장치를 명시적으로 배포하도록 요구합니다. 지금 배포하시겠습니까?"),
("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."), ("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."),
("Unknown response.", "알 수 없는 응답입니다."), ("Unknown response.", "알 수 없는 응답입니다."),
("wayland-keyboard-input-disabled-tip", "키보드 입력을 허용하시겠습니까?"), ("wayland-keyboard-input-disabled-tip", "키보드 입력을 허용하시겠습니까?"),
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용(비밀번호 포함)은 해당 컴퓨터의 다른 앱 읽을 수 있습니다."), ("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용 (비밀번호 포함)은 해당 컴퓨터의 다른 앱에서 읽을 수 있습니다."),
("wayland-keyboard-input-applies-to-tip", "이 선택이 적용되는 대상:"), ("wayland-keyboard-input-applies-to-tip", "이 선택이 적용되는 대상:"),
("wayland-soft-keyboard-input-label", "소프트 키보드 입력"), ("wayland-soft-keyboard-input-label", "소프트 키보드 입력"),
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 초기화"), ("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 재설정"),
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않"), ("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않"),
("Why this happens", " 현상이 발생하는 이유"), ("Why this happens", "이 현상이 발생하는 이유"),
("Switch display", "디스플레이 전환"), ("Switch display", "디스플레이 전환"),
("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"), ("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"),
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"), ("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
("All monitors", "모든 모니터"),
("#{} monitor", "#{} 모니터"),
("conn-e2ee-unavailable-tip", "종단 간 암호화를 확인할 수 없습니다.\n원격 장치가 여전히 설정 중일 수 있습니다. 나중에 다시 시도해 보세요.\n이런 일이 계속 발생하면 서버가 신뢰할 수 없을 수도 있습니다.\n어쨌든 계속하시겠습니까?"),
("ID whitelisting", "ID 화이트리스트"),
("Use ID whitelisting", "ID 화이트리스트 사용"),
("id_whitelist_tip", "화이트리스트에 있는 ID만 나에게 액세스할 수 있음"),
("id_whitelist_wildcard_tip", "와일드카드를 지원합니다: '*'는 임의 개수의 문자와, '?'는 정확히 한 문자와 일치합니다"),
("Invalid ID", "유효하지 않은 ID입니다"),
("Your ID is blocked by the peer", "귀하의 ID가 상대방에 의해 차단되었습니다"),
("Your ip is blocked by the peer", "귀하의 IP가 상대방에 의해 차단되었습니다"),
("id_whitelist_caveat_tip", "ID는 연결하는 클라이언트가 보고합니다. 화이트리스트는 노출을 줄이는 것으로 비밀번호나 2FA를 대체하지 않습니다"),
("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Релай Қосылым"), ("Relay Connection", "Релай Қосылым"),
("Secure Connection", "Қауіпсіз Қосылым"), ("Secure Connection", "Қауіпсіз Қосылым"),
("Insecure Connection", "Қатерлі Қосылым"), ("Insecure Connection", "Қатерлі Қосылым"),
("Continue", ""),
("Scale original", "Scale original"), ("Scale original", "Scale original"),
("Scale adaptive", "Scale adaptive"), ("Scale adaptive", "Scale adaptive"),
("General", "Жалпы"), ("General", "Жалпы"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Дисплейді ауыстыру"), ("Switch display", "Дисплейді ауыстыру"),
("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"), ("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"),
("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"), ("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"),
("All monitors", "Барлық мониторлар"),
("#{} monitor", "Монитор {}"),
("conn-e2ee-unavailable-tip", "Ұштан-ұшқа шифрлауды тексеру мүмкін болмады.\nҚашықтағы құрылғы әлі бапталып жатқан болуы мүмкін. Кейінірек қайталап көріңіз.\nЕгер бұл қайталана берсе, сервер сенімсіз болуы мүмкін.\nСонда да жалғастыру керек пе?"),
("ID whitelisting", "ID Ақ-тізімі"),
("Use ID whitelisting", "ID ақ-тізімін қолдану"),
("id_whitelist_tip", "Маған тек ақ-тізімделген ID қол жеткізе алады"),
("id_whitelist_wildcard_tip", "Қойылмалы таңбаларға қолдау көрсетіледі: '*' кез келген таңба санына, '?' дәл бір таңбаға сәйкес келеді"),
("Invalid ID", "Бұрыс ID"),
("Your ID is blocked by the peer", "Сіздің ID қарсы тараппен бұғатталған"),
("Your ip is blocked by the peer", "Сіздің IP-мекенжайыңыз қарсы тараппен бұғатталған"),
("id_whitelist_caveat_tip", "ID қосылатын клиентпен хабарланады. Ақ-тізім әсер ету аумағын азайтады және құпия сөзді немесе 2FA-ны алмастырмайды"),
("whitelist_cidr_tip", "CIDR жазбасына қолдау көрсетіледі, мысалы 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Tarpinė jungtis"), ("Relay Connection", "Tarpinė jungtis"),
("Secure Connection", "Saugus ryšys"), ("Secure Connection", "Saugus ryšys"),
("Insecure Connection", "Nesaugus ryšys"), ("Insecure Connection", "Nesaugus ryšys"),
("Continue", ""),
("Scale original", "Pakeisti originalų mastelį"), ("Scale original", "Pakeisti originalų mastelį"),
("Scale adaptive", "Pritaikomas mastelis"), ("Scale adaptive", "Pritaikomas mastelis"),
("General", "Bendra"), ("General", "Bendra"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Perjungti ekraną"), ("Switch display", "Perjungti ekraną"),
("Show monitor switch button on the main toolbar", "Rodyti monitoriaus perjungimo mygtuką pagrindinėje įrankių juostoje"), ("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"), ("Show on the minimized toolbar", "Rodyti sumažintoje įrankių juostoje"),
("All monitors", "Visi monitoriai"),
("#{} monitor", "Monitorius {}"),
("conn-e2ee-unavailable-tip", "Nepavyko patikrinti galinio šifravimo.\nNuotolinis įrenginys galbūt vis dar nustatomas. Bandykite dar kartą vėliau.\nJei tai kartojasi, serveris gali būti nepatikimas.\nVis tiek tęsti?"),
("ID whitelisting", "ID baltasis sąrašas"),
("Use ID whitelisting", "Naudoti patikimą ID sąrašą"),
("id_whitelist_tip", "Mane gali pasiekti tik baltajame sąraše esantys ID"),
("id_whitelist_wildcard_tip", "Palaikomi pakaitos simboliai: '*' atitinka bet kokį simbolių skaičių, '?' lygiai vieną simbolį"),
("Invalid ID", "Netinkamas ID"),
("Your ID is blocked by the peer", "Jūsų ID užblokavo nuotolinis įrenginys"),
("Your ip is blocked by the peer", "Jūsų IP adresą užblokavo nuotolinis įrenginys"),
("id_whitelist_caveat_tip", "ID praneša prisijungiantis klientas. Šis sąrašas sumažina atakos paviršių ir nepakeičia slaptažodžio ar 2FA"),
("whitelist_cidr_tip", "Palaikomas CIDR žymėjimas, pavyzdžiui 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Releja savienojums"), ("Relay Connection", "Releja savienojums"),
("Secure Connection", "Drošs savienojums"), ("Secure Connection", "Drošs savienojums"),
("Insecure Connection", "Nedrošs savienojums"), ("Insecure Connection", "Nedrošs savienojums"),
("Continue", ""),
("Scale original", "Mērogs oriģināls"), ("Scale original", "Mērogs oriģināls"),
("Scale adaptive", "Mērogs adaptīvs"), ("Scale adaptive", "Mērogs adaptīvs"),
("General", "Vispārīgi"), ("General", "Vispārīgi"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Pārslēgt displeju"), ("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 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ā"), ("Show on the minimized toolbar", "Rādīt minimizētajā rīkjoslā"),
("All monitors", "Visi monitori"),
("#{} monitor", "Monitors {}"),
("conn-e2ee-unavailable-tip", "Neizdevās pārbaudīt pilnīgu šifrēšanu.\nAttālā ierīce, iespējams, vēl tiek iestatīta. Mēģiniet vēlreiz vēlāk.\nJa tas turpinās, serveris var nebūt uzticams.\nVai tomēr turpināt?"),
("ID whitelisting", "ID baltais saraksts"),
("Use ID whitelisting", "Izmantot balto ID sarakstu"),
("id_whitelist_tip", "Man var piekļūt tikai baltajā sarakstā iekļautie ID"),
("id_whitelist_wildcard_tip", "Tiek atbalstītas aizstājzīmes: '*' atbilst jebkuram rakstzīmju skaitam, '?' — tieši vienai rakstzīmei"),
("Invalid ID", "Nederīgs ID"),
("Your ID is blocked by the peer", "Jūsu ID ir bloķējusi otra puse"),
("Your ip is blocked by the peer", "Jūsu IP adresi ir bloķējusi otra puse"),
("id_whitelist_caveat_tip", "ID paziņo klients, kas veido savienojumu. Baltais saraksts samazina pakļautību un neaizstāj paroli vai 2FA"),
("whitelist_cidr_tip", "Tiek atbalstīts CIDR pieraksts, piemēram 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "റിലേ കണക്ഷൻ"), ("Relay Connection", "റിലേ കണക്ഷൻ"),
("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"), ("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"),
("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"), ("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"),
("Continue", ""),
("Scale original", "ഒറിജിനൽ വലിപ്പം"), ("Scale original", "ഒറിജിനൽ വലിപ്പം"),
("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"), ("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"),
("General", "പൊതുവായവ"), ("General", "പൊതുവായവ"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"), ("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"),
("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"), ("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"),
("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"), ("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"),
("All monitors", "എല്ലാ മോണിറ്ററുകളും"),
("#{} monitor", "മോണിറ്റർ {}"),
("conn-e2ee-unavailable-tip", "എൻഡ്-ടു-എൻഡ് എൻക്രിപ്ഷൻ പരിശോധിക്കാൻ കഴിഞ്ഞില്ല.\nദൂരസ്ഥ ഉപകരണം ഇനിയും സജ്ജീകരണത്തിലായിരിക്കാം. പിന്നീട് വീണ്ടും ശ്രമിക്കുക.\nഇത് തുടർന്നാൽ സർവർ വിശ്വസനീയമല്ലായിരിക്കാം.\nഎങ്കിലും തുടരണമോ?"),
("ID whitelisting", "ID വൈറ്റ്‌ലിസ്റ്റിംഗ്"),
("Use ID whitelisting", "ID വൈറ്റ്‌ലിസ്റ്റിംഗ് ഉപയോഗിക്കുക"),
("id_whitelist_tip", "വൈറ്റ്‌ലിസ്റ്റ് ചെയ്ത ID-കൾക്ക് മാത്രമേ എന്നെ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ"),
("id_whitelist_wildcard_tip", "വൈൽഡ്കാർഡുകൾ പിന്തുണയ്ക്കുന്നു: '*' എത്ര അക്ഷരങ്ങളുമായും, '?' കൃത്യം ഒരു അക്ഷരവുമായും പൊരുത്തപ്പെടുന്നു"),
("Invalid ID", "അസാധുവായ ID"),
("Your ID is blocked by the peer", "നിങ്ങളുടെ ID മറുവശം ബ്ലോക്ക് ചെയ്തിരിക്കുന്നു"),
("Your ip is blocked by the peer", "നിങ്ങളുടെ IP വിലാസം മറുവശം ബ്ലോക്ക് ചെയ്തിരിക്കുന്നു"),
("id_whitelist_caveat_tip", "കണക്റ്റ് ചെയ്യുന്ന ക്ലയന്റാണ് ID റിപ്പോർട്ട് ചെയ്യുന്നത്. വൈറ്റ്‌ലിസ്റ്റ് എക്സ്പോഷർ കുറയ്ക്കുന്നു; പാസ്‌വേഡിനോ 2FA-യ്ക്കോ പകരമല്ല"),
("whitelist_cidr_tip", "CIDR നൊട്ടേഷൻ പിന്തുണയ്ക്കുന്നു, ഉദാഹരണത്തിന് 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Viderekoblet tilkobling"), ("Relay Connection", "Viderekoblet tilkobling"),
("Secure Connection", "Sikker tilkobling"), ("Secure Connection", "Sikker tilkobling"),
("Insecure Connection", "Usikker tilkobling"), ("Insecure Connection", "Usikker tilkobling"),
("Continue", ""),
("Scale original", "Original skalering"), ("Scale original", "Original skalering"),
("Scale adaptive", "Adaptiv skalering"), ("Scale adaptive", "Adaptiv skalering"),
("General", "Generelt"), ("General", "Generelt"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Bytt skjerm"), ("Switch display", "Bytt skjerm"),
("Show monitor switch button on the main toolbar", "Vis knapp for skjermbytte på hovedverktøylinjen"), ("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"), ("Show on the minimized toolbar", "Vis på den minimerte verktøylinjen"),
("All monitors", "Alle skjermer"),
("#{} monitor", "Skjerm {}"),
("conn-e2ee-unavailable-tip", "Ende-til-ende-kryptering kunne ikke verifiseres.\nDen eksterne enheten kan fortsatt være under oppsett. Prøv igjen senere.\nHvis dette fortsetter, kan serveren være upålitelig.\nFortsette likevel?"),
("ID whitelisting", "ID-hvitelisting"),
("Use ID whitelisting", "Bruk ID-hvitelisting"),
("id_whitelist_tip", "Kun ID-er på hvitelisten kan få adgang til meg"),
("id_whitelist_wildcard_tip", "Jokertegn støttes: '*' samsvarer med et hvilket som helst antall tegn, '?' med nøyaktig ett tegn"),
("Invalid ID", "Ugyldig ID"),
("Your ID is blocked by the peer", "ID-en din er blokkert av motparten"),
("Your ip is blocked by the peer", "IP-adressen din er blokkert av motparten"),
("id_whitelist_caveat_tip", "ID-en rapporteres av klienten som kobler til. Hvitelisten reduserer eksponeringen og erstatter ikke passord eller 2FA"),
("whitelist_cidr_tip", "CIDR-notasjon støttes, for eksempel 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relaisverbinding"), ("Relay Connection", "Relaisverbinding"),
("Secure Connection", "Beveiligde Verbinding"), ("Secure Connection", "Beveiligde Verbinding"),
("Insecure Connection", "Onveilige Verbinding"), ("Insecure Connection", "Onveilige Verbinding"),
("Continue", ""),
("Scale original", "Oorspronkelijk formaat"), ("Scale original", "Oorspronkelijk formaat"),
("Scale adaptive", "Automatisch schalen"), ("Scale adaptive", "Automatisch schalen"),
("General", "Algemeen"), ("General", "Algemeen"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Beeldscherm wisselen"), ("Switch display", "Beeldscherm wisselen"),
("Show monitor switch button on the main toolbar", "Knop voor monitorwisseling weergeven op de hoofdwerkbalk"), ("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"), ("Show on the minimized toolbar", "Weergeven op de geminimaliseerde werkbalk"),
("All monitors", "Alle monitoren"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "End-to-endversleuteling kon niet worden geverifieerd.\nHet externe apparaat wordt mogelijk nog ingesteld. Probeer het later opnieuw.\nAls dit blijft gebeuren, is de server mogelijk niet vertrouwd.\nToch doorgaan?"),
("ID whitelisting", "ID Witte Lijst"),
("Use ID whitelisting", "Gebruik een witte lijst van ID's"),
("id_whitelist_tip", "Alleen ID's op de witte lijst krijgen toegang tot mijn toestel"),
("id_whitelist_wildcard_tip", "Jokertekens worden ondersteund: '*' komt overeen met een willekeurig aantal tekens, '?' met precies één teken"),
("Invalid ID", "Ongeldig ID"),
("Your ID is blocked by the peer", "Je ID is geblokkeerd door de andere partij"),
("Your ip is blocked by the peer", "Je IP-adres is geblokkeerd door de andere partij"),
("id_whitelist_caveat_tip", "Het ID wordt gemeld door de verbindende client. De witte lijst vermindert blootstelling en vervangt het wachtwoord of 2FA niet"),
("whitelist_cidr_tip", "CIDR-notatie wordt ondersteund, bijvoorbeeld 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Połączenie przez bramkę"), ("Relay Connection", "Połączenie przez bramkę"),
("Secure Connection", "Połączenie szyfrowane"), ("Secure Connection", "Połączenie szyfrowane"),
("Insecure Connection", "Połączenie nieszyfrowane"), ("Insecure Connection", "Połączenie nieszyfrowane"),
("Continue", ""),
("Scale original", "Skalowanie oryginalne"), ("Scale original", "Skalowanie oryginalne"),
("Scale adaptive", "Dopasuj do wyświetlacza"), ("Scale adaptive", "Dopasuj do wyświetlacza"),
("General", "Ogólne"), ("General", "Ogólne"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Przełącz ekran"), ("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 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"), ("Show on the minimized toolbar", "Pokaż na zminimalizowanym pasku narzędzi"),
("All monitors", "Wszystkie ekrany"),
("#{} monitor", "Ekran {}"),
("conn-e2ee-unavailable-tip", "Nie można zweryfikować szyfrowania end-to-end.\nUrządzenie zdalne może nadal się konfigurować. Spróbuj ponownie później.\nJeśli problem będzie się powtarzał, serwer może być niezaufany.\nKontynuować mimo to?"),
("ID whitelisting", "Biała lista ID"),
("Use ID whitelisting", "Użyj białej listy ID"),
("id_whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z ID znajdujących się na białej liście"),
("id_whitelist_wildcard_tip", "Obsługiwane są symbole wieloznaczne: '*' odpowiada dowolnej liczbie znaków, '?' dokładnie jednemu znakowi"),
("Invalid ID", "Nieprawidłowe ID"),
("Your ID is blocked by the peer", "Twoje ID zostało zablokowane przez drugą stronę"),
("Your ip is blocked by the peer", "Twój adres IP został zablokowany przez drugą stronę"),
("id_whitelist_caveat_tip", "ID jest zgłaszane przez łączącego się klienta. Biała lista zmniejsza ekspozycję i nie zastępuje hasła ani 2FA"),
("whitelist_cidr_tip", "Obsługiwana jest notacja CIDR, na przykład 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexão de relé"), ("Relay Connection", "Conexão de relé"),
("Secure Connection", "Conexão segura"), ("Secure Connection", "Conexão segura"),
("Insecure Connection", "Conexão insegura"), ("Insecure Connection", "Conexão insegura"),
("Continue", ""),
("Scale original", "Escala original"), ("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptável"), ("Scale adaptive", "Escala adaptável"),
("General", "Geral"), ("General", "Geral"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Trocar de ecrã"), ("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 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"), ("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
("All monitors", "Todos os monitores"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "Não foi possível verificar a encriptação de ponta a ponta.\nO dispositivo remoto ainda pode estar a ser configurado. Tente novamente mais tarde.\nSe isto continuar a acontecer, o servidor pode não ser fidedigno.\nContinuar mesmo assim?"),
("ID whitelisting", "Whitelist de ID"),
("Use ID whitelisting", "Usar whitelist de ID"),
("id_whitelist_tip", "Somente IDs na whitelist podem me acessar"),
("id_whitelist_wildcard_tip", "São suportados carateres universais: '*' corresponde a qualquer número de carateres, '?' a exatamente um caráter"),
("Invalid ID", "ID inválido"),
("Your ID is blocked by the peer", "O seu ID está bloqueado pelo dispositivo remoto"),
("Your ip is blocked by the peer", "O seu IP está bloqueado pelo dispositivo remoto"),
("id_whitelist_caveat_tip", "O ID é comunicado pelo cliente que se liga. A whitelist reduz a exposição e não substitui a palavra-passe nem o 2FA"),
("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+19 -6
View File
@@ -112,7 +112,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Waiting", "Aguardando"), ("Waiting", "Aguardando"),
("Finished", "Concluído"), ("Finished", "Concluído"),
("Speed", "Velocidade"), ("Speed", "Velocidade"),
("Custom Image Quality", "Qualidade Visual Personalizada"), ("Custom Image Quality", "Qualidade de imagem personalizada"),
("Privacy mode", "Modo privado"), ("Privacy mode", "Modo privado"),
("Block user input", "Bloquear entrada do usuário"), ("Block user input", "Bloquear entrada do usuário"),
("Unblock user input", "Desbloquear entrada do usuário"), ("Unblock user input", "Desbloquear entrada do usuário"),
@@ -122,15 +122,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stretch", "Aumentar"), ("Stretch", "Aumentar"),
("Scrollbar", "Barra de rolagem"), ("Scrollbar", "Barra de rolagem"),
("ScrollAuto", "Rolagem automática"), ("ScrollAuto", "Rolagem automática"),
("Good image quality", "Boa qualidade de imagem"), ("Good image quality", "Melhor qualidade"),
("Balanced", "Balanceada"), ("Balanced", "Balanceada"),
("Optimize reaction time", "Otimizar tempo de resposta"), ("Optimize reaction time", "Otimizar tempo de resposta"),
("Custom", "Personalizado"), ("Custom", "Personalizada"),
("Show remote cursor", "Mostrar cursor remoto"), ("Show remote cursor", "Mostrar cursor remoto"),
("Show quality monitor", "Exibir monitor de qualidade"), ("Show quality monitor", "Exibir monitor de qualidade"),
("Disable clipboard", "Desabilitar área de transferência"), ("Disable clipboard", "Desabilitar área de transferência"),
("Lock after session end", "Bloquear após o fim da sessão"), ("Lock after session end", "Bloquear após o fim da sessão"),
("Insert Ctrl + Alt + Del", "Enviar Ctrl + Alt + Del"), ("Insert Ctrl + Alt + Del", "Enviar Ctrl+Alt+Del"),
("Insert Lock", "Bloquear computador"), ("Insert Lock", "Bloquear computador"),
("Refresh", "Atualizar"), ("Refresh", "Atualizar"),
("ID does not exist", "ID não existe"), ("ID does not exist", "ID não existe"),
@@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept and Install", "Aceitar e Instalar"), ("Accept and Install", "Aceitar e Instalar"),
("End-user license agreement", "Acordo de licença do usuário final"), ("End-user license agreement", "Acordo de licença do usuário final"),
("Generating ...", "Gerando ..."), ("Generating ...", "Gerando ..."),
("Your installation is lower version.", "Instalação desatualizada"), ("Your installation is lower version.", "Sua instalação está com uma versão desatualizada."),
("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"), ("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"),
("Listening ...", "Escutando ..."), ("Listening ...", "Escutando ..."),
("Remote Host", "Host Remoto"), ("Remote Host", "Host Remoto"),
@@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Fullscreen", "Tela Cheia"), ("Fullscreen", "Tela Cheia"),
("Mobile Actions", "Ações móveis"), ("Mobile Actions", "Ações móveis"),
("Select Monitor", "Selecionar tela"), ("Select Monitor", "Selecionar tela"),
("Control Actions", "Controlar ações"), ("Control Actions", "Ações de controle"),
("Display Settings", "Configurações de exibição"), ("Display Settings", "Configurações de exibição"),
("Ratio", "Proporção"), ("Ratio", "Proporção"),
("Image Quality", "Qualidade de imagem"), ("Image Quality", "Qualidade de imagem"),
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexão via Relay"), ("Relay Connection", "Conexão via Relay"),
("Secure Connection", "Conexão Segura"), ("Secure Connection", "Conexão Segura"),
("Insecure Connection", "Conexão Insegura"), ("Insecure Connection", "Conexão Insegura"),
("Continue", "Continuar"),
("Scale original", "Escala original"), ("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptada"), ("Scale adaptive", "Escala adaptada"),
("General", "Geral"), ("General", "Geral"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Trocar de tela"), ("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 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"), ("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
("All monitors", "Todas as telas"),
("#{} monitor", "Tela {}"),
("conn-e2ee-unavailable-tip", "Não foi possível verificar a criptografia de ponta a ponta.\nO dispositivo remoto ainda pode estar sendo configurado. Tente novamente mais tarde.\nSe isso continuar acontecendo, o servidor pode não ser confiável.\nContinuar mesmo assim?"),
("ID whitelisting", "Lista de IDs permitidos"),
("Use ID whitelisting", "Utilizar lista de IDs permitidos"),
("id_whitelist_tip", "Somente IDs confiáveis podem me acessar"),
("id_whitelist_wildcard_tip", "Curingas são suportados: '*' corresponde a qualquer número de caracteres, '?' a exatamente um caractere"),
("Invalid ID", "ID inválido"),
("Your ID is blocked by the peer", "Seu ID foi bloqueado pelo dispositivo remoto"),
("Your ip is blocked by the peer", "Seu IP foi bloqueado pelo dispositivo remoto"),
("id_whitelist_caveat_tip", "O ID é informado pelo cliente que se conecta. A lista reduz a exposição e não substitui a senha ou o 2FA"),
("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+13
View File
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexiune prin retransmisie"), ("Relay Connection", "Conexiune prin retransmisie"),
("Secure Connection", "Conexiune securizată"), ("Secure Connection", "Conexiune securizată"),
("Insecure Connection", "Conexiune nesecurizată"), ("Insecure Connection", "Conexiune nesecurizată"),
("Continue", ""),
("Scale original", "Dimensiune originală"), ("Scale original", "Dimensiune originală"),
("Scale adaptive", "Scalare automată"), ("Scale adaptive", "Scalare automată"),
("General", "General"), ("General", "General"),
@@ -761,5 +762,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Comută afișajul"), ("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 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ă"), ("Show on the minimized toolbar", "Afișează în bara de instrumente minimizată"),
("All monitors", "Toate monitoarele"),
("#{} monitor", "Monitor {}"),
("conn-e2ee-unavailable-tip", "Criptarea end-to-end nu a putut fi verificată.\nDispozitivul la distanță poate fi încă în curs de configurare. Încercați din nou mai târziu.\nDacă acest lucru continuă, serverul poate să nu fie de încredere.\nContinuați oricum?"),
("ID whitelisting", "Listă de ID-uri autorizate"),
("Use ID whitelisting", "Folosește lista de ID-uri autorizate"),
("id_whitelist_tip", "Doar ID-urile autorizate pot accesa acest dispozitiv"),
("id_whitelist_wildcard_tip", "Sunt acceptate metacaractere: '*' corespunde oricărui număr de caractere, '?' exact unui caracter"),
("Invalid ID", "ID nevalid"),
("Your ID is blocked by the peer", "ID-ul tău este blocat de dispozitivul de la distanță"),
("Your ip is blocked by the peer", "Adresa ta IP este blocată de dispozitivul de la distanță"),
("id_whitelist_caveat_tip", "ID-ul este raportat de clientul care se conectează. Lista albă reduce expunerea și nu înlocuiește parola sau 2FA"),
("whitelist_cidr_tip", "Notația CIDR este acceptată, de exemplu 192.168.1.0/24"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

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