Compare commits

...

52 Commits

Author SHA1 Message Date
rustdesk
04faf21c78 feat: keyboard shortcuts in remote sessions
Add an opt-in keyboard-shortcut system that triggers session
actions (Send Ctrl+Alt+Del, Toggle Fullscreen, Switch Display,
Screenshot, Switch Tab, etc.) via three-modifier combinations
during a remote session.

Architecture
- Native: src/keyboard/shortcuts.rs intercepts at the encoder
  layer (process_event and process_event_with_session), so the
  feature is input-source-independent. Bindings persist as a
  single JSON blob in LocalConfig.
- Web: matching + keydown intercept live in the separate hand-
  written TS client at flutter/web/js/ (gitignored, not in this
  repo). flutter/lib/web/bridge.dart::mainInit registers
  window.onShortcutTriggered so the JS matcher can dispatch
  back into the active session's ShortcutModel; the bridge's
  mainReloadKeyboardShortcuts forwards to a JS reloadShortcuts
  on settings writes.
- Three-modifier prefix (Ctrl+Alt+Shift; Cmd+Option+Shift on
  macOS/iOS) sidesteps the need for a pass-through toggle.
- Flutter native path threads the explicit per-call SessionID
  for tab-precise routing; rdev path uses globally-current
  session.

UI
- Settings -> General -> Keyboard Shortcuts opens a dedicated
  configuration page; desktop and mobile share a body widget.
- Recording dialog with live capture, prefix validation, and a
  conflict-replace flow.
- Toolbar menu items display the bound shortcut inline.
- Default bindings (adapted from AnyDesk):
    +Del    Send Ctrl+Alt+Del
    +Enter  Toggle Fullscreen
    +Left/Right  Switch Display Prev/Next
    +P      Screenshot
    +1..9   Switch Session Tab

Other
- AGENTS.md: documented (a) flutter_rust_bridge_codegen needs
  a pinned version + Dart bridge wrappers should be hand-
  written, and (b) the Web-target split where flutter/web/js/
  is the runtime owner on Web rather than wasm-compiled Rust.
- 38 new i18n strings in src/lang/en.rs with Chinese
  translations in src/lang/cn.rs.

Refs discussion #1933.
2026-04-28 15:48:12 +08:00
KaneBarns
bfd31d21e4 Update build.py (#11341) 2026-04-28 15:08:10 +08:00
Amirhosein Akhlaghpoor
590296b297 fix: iPad mouse down detection for physical mouse input (#14515)
* fix: iPad mouse down detection

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* fix(ipad): remove redundant check

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

* fix(ipad): Simple refactor

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

---------

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-28 15:03:41 +08:00
eason
ee8cc0c06b fix(linux): prevent X11 BadWindow crash in get_focused_display (#14561)
* fix(linux): prevent X11 BadWindow crash in get_focused_display

When the active window is destroyed between xdo_get_active_window and
xdo_get_window_location/xdo_get_window_size calls, the default X11
error handler terminates the process with a BadWindow error. This
causes the rustdesk --server process to crash and the remote session
to disconnect and reconnect every time the user closes a window.

Install a custom X error handler around the xdo calls that catches
BadWindow errors and returns gracefully instead of crashing.

Fixes: https://github.com/rustdesk/rustdesk/issues/9003

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
Signed-off-by: easonysliu <easonysliu@tencent.com>

* fix(linux): prevent BadWindow crash in focus display lookup

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

---------

Signed-off-by: easonysliu <easonysliu@tencent.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude (claude-opus-4-6) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-28 11:04:29 +08:00
s1korrrr
99b565ef40 fix(iOS): preserve local pasteboard sync from Windows hosts (#14659)
* fix(ios): accept windows clipboard updates locally

Signed-off-by: Rafal <mrsikorarafal@gmail.com>

* docs: document clipboard text helpers

* fix(iOS): sync clipboard, debug

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

---------

Signed-off-by: Rafal <mrsikorarafal@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-28 10:55:28 +08:00
fufesou
1e6a3dc644 fix(android): waiting for image, one cause (#14919)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-27 22:37:22 +08:00
s1korrrr
5b7ad339b8 fix(iPad): keep touch gestures with external mouse (#14652)
* fix(ipad): keep touch gestures with external mouse

Signed-off-by: Rafal <mrsikorarafal@gmail.com>

* fix(mobile): touch gesture on physical mouse connected

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

* fix(ipad): revert 9ee100b53e

keep touch gestures with external mouse

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

* fix(mobile): align view camera page with remote page

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

---------

Signed-off-by: Rafal <mrsikorarafal@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-27 19:44:35 +08:00
Sergiusz Michalik
7308c448f1 fix(client): serialize X11 keyboard grab and debounce focus feedback (#14836)
* fix(client): serialize X11 keyboard grab and debounce focus feedback

When two RustDesk sessions run fullscreen on separate monitors on
Linux/X11, keyboard input gets stuck on the wrong session or stops
working entirely. This happens because each Flutter isolate calls
change_grab_status concurrently, racing on KEYBOARD_HOOKED and the
rdev grab channel.

Additionally, XGrabKeyboard causes a focus-change feedback loop:
grab shifts focus away from the Flutter window, triggering PointerExit,
which releases the grab, restoring focus, triggering PointerEnter,
which re-grabs -- cycling at ~10 Hz and blocking keyboard input.

Fix by:
- Serializing grab transitions with a mutex and tracking the owning
  session (by lc.session_id), so a stale Wait from session A cannot
  clobber session B's freshly acquired grab.
- Debouncing Wait events (300 ms) from the same session that just
  acquired the grab, breaking the X11 focus feedback loop.
- Refreshing the debounce timer on idempotent Run calls (enterView
  while already owner), keeping the grab stable during normal use.

Signed-off-by: Sergiusz Michalik <github@latens.me>

* fix(client): add deferred release and dedup for debounced Wait

When a Wait is debounced (within 300ms of grab acquisition), schedule
a deferred release thread that re-checks after the debounce window.
If no new Run refreshed the grab, the deferred thread releases it,
ensuring a genuine leave within the debounce window is not lost.

Add a deferred_pending flag to GrabOwnerState to prevent spawning
redundant threads during the X11 focus feedback loop.

Signed-off-by: Sergiusz Michalik <github@latens.me>

* fix(client): use window-scoped ID and fix deferred-release re-arming

Address PR review feedback:
- Use per-window UUID instead of connection-scoped lc.session_id so two
  windows viewing the same peer get distinct grab owners
- Reset deferred_pending on both idempotent Run refresh and owner
  handoff, so a subsequent Wait can always spawn a fresh timer
- Replace manual Default impl with derive

* fix(client): recover from poisoned mutex instead of panicking

* docs: clarify cross-platform rationale for GrabOwnerState

* fix(client): only clear deferred_pending when timer snapshot matches

* fix(client): use full u128 window ID, downgrade grab logs to debug

- Widen GrabOwnerState.owner to u128 to avoid theoretical collision
  from truncating a 128-bit UUID to 64 bits
- Downgrade all grab transition log::info! to log::debug! to reduce
  log noise during routine window switches
- Clear deferred_pending on post-debounce release path to maintain
  the "deferred_pending => timer in flight" invariant

* fix(client): gate GRAB_DEBOUNCE_MS with cfg(target_os = "linux")

* fix(grab): release grabbed keys without clobbering new owner state

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

* fix(keyboard): Simple refactor

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

---------

Signed-off-by: Sergiusz Michalik <github@latens.me>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-26 22:46:41 +08:00
Amirhosein Akhlaghpoor
c8ba99d1a1 flutter: shift after one shot IME capitalization (#14695)
* flutter: shift after one shot IME capitalization

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* flutter: clarify stale mobile shift handling

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* fix(android): gboard shift stuck

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

* fix(android): gboard shift stuck, remove unused param

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

* fix(android): gboard shift stuck, release shift before sending events

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

* chore(flutter): document stale mobile shift release flow

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

---------

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-04-26 22:44:26 +08:00
Azhar
5ea6714db8 Fix: replace unwrap() with proper error handling in CLI password prompt (#14910)
Signed-off-by: bunnysayzz <stfuazzo@gmail.com>
2026-04-26 21:28:05 +08:00
fufesou
3a1622e8b5 refact(AGENTS.md): code rules, tokio (#14911)
* refact(AGENTS.md): code rules, tokio

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

* Update AGENTS.md

* Update AGENTS.md

* Update AGENTS.md

* Update AGENTS.md

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-04-26 21:25:31 +08:00
Sergiusz Michalik
38f1300717 fix(linux): enable mouse side buttons in remote sessions (#14848)
* fix(linux): enable mouse side buttons in remote sessions

Flutter's Linux embedder never delivers X11 button 8/9 (back/forward)
events to Dart, so mouse side buttons were silently dropped in remote
sessions.

Intercept these buttons at the GDK level via button-press/release-event
handlers on all windows (main + sub-windows) and forward them through
a dedicated platform channel to the active InputModel session.

Also add a defensive XSetPointerMapping call during enigo init to
extend the X11 core pointer button map to 9 buttons on servers where
it is smaller (e.g. minimal X server configurations).

* fix: address review feedback for side button support

- Use XOpenDisplay/XCloseDisplay instead of reading Display* from
  xdo_t's private struct layout at offset 0 (fragile ABI assumption)
- Track side button down ownership per button via a Map instead of a
  single slot, preventing cross-button mismatch on overlapping presses

* fix: gate side buttons on view-only and fix teardown

- Skip side button events in view-only sessions (consistent with
  other mouse entry points)
- Release held side buttons on session close to avoid stuck buttons
  on the remote
- Drop unpaired 'up' events instead of falling back to the active
  model, which could send to the wrong session

* docs: add clarifying comments from review feedback

- Note global scope of XSetPointerMapping and that it runs once
  via lazy_static singleton
- Clarify sub-window callback is safe on X11-only builds
- Document per-isolate design of initSideButtonChannel

* fix: replace broken XSetPointerMapping with diagnostic check

XSetPointerMapping requires the length to match XGetPointerMapping's
return value - it cannot extend the button count. The previous code
would trigger a BadValue X error on servers with fewer than 9 buttons.

Replace with a diagnostic-only check that logs whether the core
pointer has enough buttons for side button simulation. RustDesk's
uinput "Mouse passthrough" device already provides the needed buttons
in practice.

Also add .catchError to fire-and-forget side button releases during
session teardown to prevent unhandled async errors.

* fix: ensure side button releases bypass permission checks

If permissions change between button down and up (e.g. keyboardPerm
revoked, view-only toggled), sendMouse's early return would suppress
the release, leaving a stuck button on the remote.

Add _sendMouseUnchecked that bypasses permission checks, used for:
- Side button 'up' events (matching a recorded 'down')
- Forced releases during session teardown

Gate all permission checks (isViewOnly, keyboardPerm, isViewCamera)
at the 'down' entry point before recording in _sideButtonDownModels.

* fix: add NULL guards and avoid blocking platform channel handler

- Add NULL checks for FL_VIEW cast and channel creation in
  on_subwindow_created (review feedback from fufesou)
- Use fire-and-forget (unawaited) for _sendMouseUnchecked calls
  inside the platform channel handler to avoid blocking platform
  messages when sessionSendMouse is slow (review feedback from Copilot)

* fix: remove circular import and skip X11 check on Wayland

- Move initSideButtonChannel() call from initEnv() in main.dart to
  the InputModel constructor, removing the circular import between
  main.dart and input_model.dart
- Skip check_x11_button_map() when DISPLAY is not set to avoid
  noisy warnings on pure Wayland environments
2026-04-25 12:46:05 +08:00
Nawer
03e351ac61 feat(i18n): Complete and fix french translations (#14890) 2026-04-24 18:38:34 +08:00
fufesou
6cb323725b fix(sicter): control side, privacy mode (#14880)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-24 14:35:58 +08:00
Aliaksandr Kliujeŭ
5d0533f0d4 Update Balarusian strings (#14842)
* Update Balarusian strings

* BE: fix typos

* BE: fix ў-related typos
2026-04-23 23:52:43 +08:00
Re*Index. (ot_inc)
e0c5e1483e Update Japanese translate (#14838)
* Update ja.rs

* Update ja.rs

* Fix typo
2026-04-23 23:52:21 +08:00
Leo Louis
47e4c65d8e Update print statement from 'Hello' to 'Goodbye' (#14754) 2026-04-22 18:06:37 +08:00
Leo Louis
9bc1ce52af Add Malayalam language support (#14753)
* Add Malayalam language support

* Fix syntax error in language list for Malayalam

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-04-22 18:06:10 +08:00
Leo Louis
348d1b46e1 Add Hindi language support with translations (#14746)
* Add Hindi language support with translations

* Update print statement from 'Hello' to 'Goodbye'
2026-04-22 18:04:37 +08:00
Leo Louis
1a41b3ac11 Add Hindi language module and translation support (#14745)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-04-22 18:04:09 +08:00
rustdesk
b239535009 refactor per code review 2026-04-22 01:41:13 +08:00
RustDesk
5fd20f808c fix safari-oidc https://github.com/rustdesk/rustdesk/issues/14861 (#14867) 2026-04-22 01:29:15 +08:00
rustdesk
803ac8cc4e save cargo build size 2026-04-21 17:34:05 +08:00
John Eismeier
4a50bc6fc2 Propose fix some typos (#14857)
Signed-off-by: John E <jeis4wpi@outlook.com>
2026-04-21 16:27:39 +08:00
fufesou
e8a1b7fe21 fix: build (#14846)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-20 10:05:32 +08:00
21pages
ac124c0680 flutter: improve address book pull error handling (#14813)
* flutter: improve address book pull error handling

Summary:
  - Show error messages when fetching the address book list fails.
  - After the initial fetch, switching back to the AB tab no longer re-fetches it, even if an error occurred or the error banner was dismissed.

  Tested:
  - Self-hosted server:
    - normal
    - 403 responses
    - legacy address book mode
  - Public server
  - Verified that switching tabs no longer re-fetches AB after the initial fetch, regardless of whether an error occurred or the error banner was cleared.

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

* use resp.statusCode in address book json decoding

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

* flutter: clear address book list errors on reset

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

* flutter: clear address book pull errors consistently

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-04-18 11:19:32 +08:00
Luca-rickrolled-himself
91aff3ffd1 Complete and correct Romanian (ro) translations (#14837)
* Complete and correct Romanian (ro) translations

- Fill in all previously empty translation strings
- Fix plural form: "fișier" → "fișiere" (files)
- Fix "Receive" → "Primește" (was incorrectly using "Acceptă")
- Fix "Too frequent" → "Prea frecvent" (removed erroneous extra word)
- Fix "Note" → "Notă" (was translated as verb instead of noun)
- Fix "Use both passwords" → "Folosește ambele parole" ("programe" typo)
- Fix "Automatically record incoming sessions" → "sesiunile primite" (not "viitoare")
- Fix typo "neautoriztă" → "neautorizată" (Connection not allowed)
- Fix typo "dispozivul" → "dispozitivul" (Restart remote device)
- Fix leading whitespace in "Username" translation
- Fix "FPS" → keep as "FPS" (was incorrectly translated as "CPS")
- Fix "Forget Password" → "Parolă uitată" (command form was grammatically wrong)

* Fix typo in Romanian translation for accessibility tip

* unify informal register and fix subjunctive typo
2026-04-18 10:55:18 +08:00
John Fowler
642c281ad0 Update hu.rs (#14816)
New string translation and fixes.
2026-04-17 12:44:24 +08:00
fufesou
1e9c4d04f1 fix(mobile): deeplink, disable by default (#14824)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-16 23:21:14 +08:00
21pages
9f817714fe fix(client): stop retrying on restricted mobile access errors (#14797)
Treat "Access to mobile devices is restricted in your country"
  as a non-retriable connection error so the error dialog does not
  trigger reconnect attempts.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-04-15 21:40:03 +08:00
pallab-js
091f2c6135 impl(cm): implement change_theme and change_language callbacks (#14782)
* docs: fix typos in documentation and code comments

- Fix 'seperated' -> 'separated' in remote_input.dart
- Fix 'seperators' -> 'separators' in fuse/cs.rs
- Update outdated 'OSX' -> 'macOS' in virtual display README

Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>

* impl(cm): implement change_theme and change_language callbacks

These callbacks were previously empty TODO stubs.
Now they properly invoke the Sciter UI handlers to notify
the UI when theme or language changes occur.

Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>

---------

Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>
2026-04-15 17:35:51 +08:00
rustdesk
91de51290d add microsoft oidc logo 2026-04-15 14:39:46 +08:00
rustdesk
68fa0466c8 improved oidc login error 2026-04-15 14:36:03 +08:00
Leo Louis
28e303576c Add support for Gujarati language in lang.rs (#14751) 2026-04-14 14:21:27 +08:00
Leo Louis
2d41b3e80d Add Gujarati language support with translations (#14752) 2026-04-14 14:21:10 +08:00
Andrzej Rudnik
ffd2d26c1a Update pl.rs (#14775) 2026-04-14 14:20:35 +08:00
Leo Louis
a8dc6fc632 Fix capture method return type in Recorder trait (#14748) 2026-04-13 13:04:43 +08:00
Leo Louis
771cb4ebd7 Update capture function return type for PixelProvider (#14747) 2026-04-13 13:03:35 +08:00
fufesou
2f694c0eb2 fix: file transfer, path traversal (#14678)
* fix: file transfer, path traversal

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

* fix(fs): remove stale files

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

* fix(fs): update_folder_files() after set_files()

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

* fix(fs): reduce .clone()

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

* fix(fs): undo checking "done message for unkown id"

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

* fix(fs): refactor

1. Hide `files` in `new_write()`.
2. Use `set_files()` to validate `files` before writing.

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

* fix(fs): comments

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

* fix(fs): Remove redundant checks

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

* fix(fs): update hbb_common

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-10 18:00:11 +08:00
21pages
8dea347a21 add brute-force protection for one-time password (#14682)
* add brute-force protection for temporary password

  Rotate the temporary password after repeated failed login attempts
  within one minute, and reset the failure window after successful
  authentication.

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

* replace LazyLock with lazy_static

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

* read temporary password after locking failure state

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

* server: rotate temporary passwords after 10 consecutive failures

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

* server: clarify temporary password failure counter comment

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-04-09 17:14:21 +08:00
rustdesk
0cf3e8ed40 improve agent md 2026-04-09 15:12:57 +08:00
21pages
9d3bc7d9e6 fix switch sides for macOS peers (#14661)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-04-07 23:39:24 +08:00
🌐 Qusai ALBahri 🌱
e0427bdc77 Translate UI strings to Arabic in ar.rs (#14694) 2026-04-06 18:27:14 +08:00
fufesou
9cf1338dc4 fix(win): exe icon path (#14686)
* fix(win): exe icon path

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

* fix(win): Simple refactor

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-04 22:54:13 +08:00
RustDesk
4e30ee8d1c tcp proxy (#14633)
* tcp proxy

* fix per review

* fix per review

* Suppress secure_tcp info logs for TCP proxy requests

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

* copilot review: redact tcp proxy logs, dedupe headers, and avoid body clone

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

* format common.rs

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

* copilot review: test function name

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

* copilot review: format IPv6 tcp proxy log targets correctly

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

* copilot review: normalize HTTP method before direct request dispatch

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

* review: extract fallback helper, fix Content-Type override, add overall timeout

- Extract duplicated TCP proxy fallback logic into generic
  `with_tcp_proxy_fallback` helper used by both `post_request` and
  `http_request_sync`, eliminating code drift risk
- Allow caller-supplied Content-Type to override the default in
  `parse_simple_header` instead of silently dropping it
- Take body by reference in `post_request_http` to avoid eager clone
  when no fallback is needed
- Wrap entire `tcp_proxy_request` flow (connect + handshake + send +
  receive) in an overall timeout to prevent indefinite stalls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* review: make is_public case-insensitive and cover mixed-case rustdesk URLs

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

* oidc: route auth requests through shared HTTP/tcp-proxy path while keeping TLS warmup

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

* refactor: replace unused TryFrom<Response> with HbbHttpResponse::parse method

  Remove TryFrom<Response> impl that was never called and replace the
  private parse_hbb_http_response helper in account.rs with a public
  parse() method on HbbHttpResponse, eliminating code duplication.

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 23:13:05 +08:00
Alex Rijckaert
cca6a5fe12 Update Dutch translations (#14654) 2026-04-01 18:10:39 +08:00
VenusGirl❤
9e4b7fca4d Update Korean (#14644) 2026-03-31 21:34:35 +08:00
XLion
d135c58ead Update tw.rs (#14643) 2026-03-31 21:26:00 +08:00
Mr-Update
de194417d4 Update de.rs (#14640) 2026-03-31 21:25:05 +08:00
solokot
d01ce3173f Update ru.rs (#14636) 2026-03-30 22:37:35 +08:00
bilimiyorum
010a54d1c9 Update tr.rs (#14628)
New string entries
2026-03-29 23:02:53 +08:00
bovirus
f557fc94fa Italian language update (#14626) 2026-03-28 13:02:09 +08:00
86 changed files with 7059 additions and 1216 deletions

4
.gitignore vendored
View File

@@ -55,4 +55,6 @@ examples/**/target/
vcpkg_installed
flutter/lib/generated_plugin_registrant.dart
libsciter.dylib
flutter/web/
flutter/web/
# Local git worktrees
.worktrees/

86
AGENTS.md Normal file
View File

@@ -0,0 +1,86 @@
# RustDesk Guide
## Project Layout
### Directory Structure
* `src/` Rust app
* `src/server/` audio / clipboard / input / video / network
* `src/platform/` platform-specific code
* `src/ui/` legacy Sciter UI (deprecated)
* `flutter/` current UI
* `libs/hbb_common/` config / proto / shared utils
* `libs/scrap/` screen capture
* `libs/enigo/` input control
* `libs/clipboard/` clipboard
* `libs/hbb_common/src/config.rs` all options
### Key Components
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
### UI Architecture
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
- **Modern UI**: Flutter-based - files in `flutter/`
- Desktop: `flutter/lib/desktop/`
- Mobile: `flutter/lib/mobile/`
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
## Rust Rules
* Avoid `unwrap()` / `expect()` in production code.
* Exceptions:
* tests;
* lock acquisition where failure means poisoning, not normal control flow.
* Otherwise prefer `Result` + `?` or explicit handling.
* Do not ignore errors silently.
* Avoid unnecessary `.clone()`.
* Prefer borrowing when practical.
* Do not add dependencies unless needed.
* Keep code simple and idiomatic.
## Tokio Rules
* Assume a Tokio runtime already exists.
* Never create nested runtimes.
* Never call `Runtime::block_on()` inside Tokio / async code.
* Do not hide runtime creation inside helpers or libraries.
* Do not hold locks across `.await`.
* Prefer `.await`, `tokio::spawn`, channels.
* Use `spawn_blocking` or dedicated threads for blocking work.
* Do not use `std::thread::sleep()` in async code.
## Flutter Rust Bridge
* Do **not** run `flutter_rust_bridge_codegen` — it requires a specific pinned version that is not easy to set up locally.
* When adding new FFI functions in `src/flutter_ffi.rs`, hand-write the corresponding Dart wrappers instead of regenerating.
* Web bridge (committed): edit `flutter/lib/web/bridge.dart` directly. Follow the existing patterns there for `SyncReturn<T>` / `Future<T>` and the `dart:js` glue.
* Native bridge (`flutter/lib/generated_bridge.dart`, `src/bridge_generated.rs`, `src/bridge_generated.io.rs`): these are gitignored and regenerated by the project's CI codegen. Manually editing them locally is fine for development testing, but those edits do not persist into commits.
## Web (Flutter Web) Architecture
Flutter Web in this repo is **not** "Dart compiled to JS via Flutter alone". The runtime is split:
* **Native targets (Win/Mac/Linux/Android/iOS)**: Rust drives sessions via `flutter_rust_bridge`; Dart only renders UI.
* **Web target**: Rust does **not** run. There is a separate hand-written TypeScript / JavaScript client at `flutter/web/js/` (gitignored — not present in this repo, lives in the maintainer's local tree). It owns connection, codec, keyboard, clipboard, etc. — basically a JS port of the Rust client. The Dart UI talks to it through `flutter/lib/web/bridge.dart`, which uses `dart:js` to call JS-side functions and to register Dart-side callbacks on `window.*`.
Implications when adding any session-runtime feature (keyboard, clipboard, audio, …):
* The Rust implementation in `src/` is for **native only**. Don't try to compile it to wasm.
* The matching Web-side logic must be written in TS/JS under `flutter/web/js/src/`. It's a translation of the Rust logic, usually simpler — Web is single-window, so any per-session-id plumbing in Rust collapses to a single global on Web.
* `flutter/lib/web/bridge.dart` is the only place where Dart sees JS. Other Dart code stays platform-agnostic and goes through `bind`. Don't sprinkle `if (isWeb)` runtime branches in shared Dart files to call Web-specific logic — put the platform divergence in the bridge.
* For JS → Dart events (e.g., a Web matcher firing), the convention is: Dart sets `js.context['onFooBar'] = (...) {...}` once at startup (typically in `mainInit`); the JS side calls `window.onFooBar(...)`. See `onLoadAbFinished`, `onLoadGroupFinished` for reference.
* The maintainer cannot easily run `flutter_rust_bridge_codegen`, so when a new FFI function lands in `src/flutter_ffi.rs`:
1. add the Web counterpart to `flutter/lib/web/bridge.dart` by hand;
2. note that on the Web target it may need to be a no-op or a JS bridge call rather than a real Rust invocation.
## Editing Hygiene
* Change only what is required.
* Prefer the smallest valid diff.
* Do not refactor unrelated code.
* Do not make formatting-only changes.
* Keep naming/style consistent with nearby code.

View File

@@ -1,91 +1 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Build Commands
- `cargo run` - Build and run the desktop application (requires libsciter library)
- `python3 build.py --flutter` - Build Flutter version (desktop)
- `python3 build.py --flutter --release` - Build Flutter version in release mode
- `python3 build.py --hwcodec` - Build with hardware codec support
- `python3 build.py --vram` - Build with VRAM feature (Windows only)
- `cargo build --release` - Build Rust binary in release mode
- `cargo build --features hwcodec` - Build with specific features
### Flutter Mobile Commands
- `cd flutter && flutter build android` - Build Android APK
- `cd flutter && flutter build ios` - Build iOS app
- `cd flutter && flutter run` - Run Flutter app in development mode
- `cd flutter && flutter test` - Run Flutter tests
### Testing
- `cargo test` - Run Rust tests
- `cd flutter && flutter test` - Run Flutter tests
### Platform-Specific Build Scripts
- `flutter/build_android.sh` - Android build script
- `flutter/build_ios.sh` - iOS build script
- `flutter/build_fdroid.sh` - F-Droid build script
## Project Architecture
### Directory Structure
- **`src/`** - Main Rust application code
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
- `src/server/` - Audio/clipboard/input/video services and network connections
- `src/client.rs` - Peer connection handling
- `src/platform/` - Platform-specific code
- **`flutter/`** - Flutter UI code for desktop and mobile
- **`libs/`** - Core libraries
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
- `libs/scrap/` - Screen capture functionality
- `libs/enigo/` - Platform-specific keyboard/mouse control
- `libs/clipboard/` - Cross-platform clipboard implementation
### Key Components
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
### UI Architecture
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
- **Modern UI**: Flutter-based - files in `flutter/`
- Desktop: `flutter/lib/desktop/`
- Mobile: `flutter/lib/mobile/`
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
## Important Build Notes
### Dependencies
- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
- Set `VCPKG_ROOT` environment variable
- Download appropriate Sciter library for legacy UI support
### Ignore Patterns
When working with files, ignore these directories:
- `target/` - Rust build artifacts
- `flutter/build/` - Flutter build output
- `flutter/.dart_tool/` - Flutter tooling files
### Cross-Platform Considerations
- Windows builds require additional DLLs and virtual display drivers
- macOS builds need proper signing and notarization for distribution
- Linux builds support multiple package formats (deb, rpm, AppImage)
- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
### Feature Flags
- `hwcodec` - Hardware video encoding/decoding
- `vram` - VRAM optimization (Windows only)
- `flutter` - Enable Flutter UI
- `unix-file-copy-paste` - Unix file clipboard support
- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
### Config
All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
- Settings
- Local
- Display
- Built-in
AGENTS.md

View File

@@ -245,3 +245,6 @@ panic = 'abort'
strip = true
#opt-level = 'z' # only have smaller size after strip
rpath = true
[profile.dev]
debug = 1

1
GEMINI.md Normal file
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -512,7 +512,7 @@ def main():
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
elif os.path.isfile('/usr/bin/pacman'):
# pacman -S -needed base-devel
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)

143
docs/CODE_OF_CONDUCT-FR.md Normal file
View File

@@ -0,0 +1,143 @@
# Code de conduite des contributeurs
## Notre engagement
En tant que membres, contributeurs et responsables, nous nous engageons à faire
de la participation à notre communauté une expérience exempte de harcèlement pour
tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou
invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité
et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut
socio-économique, de la nationalité, de l'apparence personnelle, de la race, de
la religion ou de l'identité et de l'orientation sexuelle.
Nous nous engageons à agir et à interagir de manière à contribuer à une
communauté ouverte, accueillante, diversifiée, inclusive et saine.
## Nos standards
Exemples de comportements qui contribuent à un environnement positif pour notre
communauté :
* Faire preuve d'empathie et de bienveillance envers les autres
* Respecter les opinions, les points de vue et les expériences différents
* Donner et accepter gracieusement les retours constructifs
* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos
erreurs et apprendre de l'expérience
* Se concentrer sur ce qui est le mieux non seulement pour nous en tant
qu'individus, mais pour l'ensemble de la communauté
Exemples de comportements inacceptables :
* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou
avances sexuelles de quelque nature que ce soit
* Le trolling, les commentaires insultants ou désobligeants, et les attaques
personnelles ou politiques
* Le harcèlement public ou privé
* La publication d'informations privées d'autrui, telles qu'une adresse physique
ou électronique, sans autorisation explicite
* Tout autre comportement qui pourrait raisonnablement être considéré comme
inapproprié dans un cadre professionnel
## Responsabilités en matière d'application
Les responsables de la communauté sont chargés de clarifier et d'appliquer nos
standards de comportement acceptable et prendront des mesures correctives
appropriées et équitables en réponse à tout comportement qu'ils jugent
inapproprié, menaçant, offensant ou nuisible.
Les responsables de la communauté ont le droit et la responsabilité de
supprimer, modifier ou rejeter les commentaires, commits, code, modifications
du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de
conduite, et communiqueront les raisons de leurs décisions de modération le cas
échéant.
## Portée
Ce Code de conduite s'applique dans tous les espaces communautaires, et
s'applique également lorsqu'une personne représente officiellement la communauté
dans les espaces publics. Les exemples de représentation de notre communauté
incluent l'utilisation d'une adresse e-mail officielle, la publication via un
compte de réseau social officiel, ou le fait d'agir en tant que représentant
désigné lors d'un événement en ligne ou hors ligne.
## Application
Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent
être signalés aux responsables de la communauté chargés de l'application à
[info@rustdesk.com](mailto:info@rustdesk.com).
Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et
équitable.
Tous les responsables de la communauté sont tenus de respecter la vie privée et
la sécurité de la personne ayant signalé un incident.
## Directives d'application
Les responsables de la communauté suivront ces Directives d'impact communautaire
pour déterminer les conséquences de toute action qu'ils jugent en violation de ce
Code de conduite :
### 1. Correction
**Impact communautaire** : Utilisation d'un langage inapproprié ou autre
comportement jugé non professionnel ou indésirable dans la communauté.
**Conséquence** : Un avertissement écrit et privé de la part des responsables de
la communauté, expliquant la nature de la violation et pourquoi le comportement
était inapproprié. Des excuses publiques peuvent être demandées.
### 2. Avertissement
**Impact communautaire** : Une violation par un incident isolé ou une série
d'actions.
**Conséquence** : Un avertissement avec des conséquences en cas de comportement
répété. Aucune interaction avec les personnes impliquées, y compris les
interactions non sollicitées avec les personnes chargées d'appliquer le Code de
conduite, pendant une période déterminée. Cela inclut d'éviter les interactions
dans les espaces communautaires ainsi que dans les canaux externes comme les
réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion
temporaire ou permanente.
### 3. Exclusion temporaire
**Impact communautaire** : Une violation grave des standards communautaires, y
compris un comportement inapproprié persistant.
**Conséquence** : Une exclusion temporaire de toute interaction ou communication
publique avec la communauté pendant une période déterminée. Aucune interaction
publique ou privée avec les personnes impliquées, y compris les interactions non
sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est
autorisée pendant cette période. Le non-respect de ces conditions peut entraîner
une exclusion permanente.
### 4. Exclusion permanente
**Impact communautaire** : Démontrer un schéma de violation des standards
communautaires, y compris un comportement inapproprié persistant, le harcèlement
d'une personne, ou une agression envers des catégories de personnes ou leur
dénigrement.
**Conséquence** : Une exclusion permanente de toute interaction publique au sein
de la communauté.
## Attribution
Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0,
disponible à l'adresse
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Les Directives d'impact communautaire ont été inspirées par
[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC].
Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la
FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions
sont disponibles à l'adresse
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

55
docs/CONTRIBUTING-FR.md Normal file
View File

@@ -0,0 +1,55 @@
# Contribuer à RustDesk
RustDesk accueille les contributions de tous. Voici les directives si vous
envisagez de nous aider :
## Contributions
Les contributions à RustDesk ou à ses dépendances doivent être soumises sous
forme de pull requests GitHub. Chaque pull request sera examinée par un
contributeur principal (une personne ayant la permission d'intégrer des
correctifs) et sera soit intégrée dans la branche principale, soit accompagnée
de retours sur les modifications requises. Toutes les contributions doivent
suivre ce format, même celles des contributeurs principaux.
Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en
commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela
permet d'éviter les efforts en double de la part des contributeurs sur la même
issue.
## Liste de vérification pour les pull requests
- Partez de la branche master et, si nécessaire, effectuez un rebase sur la
branche master actuelle avant de soumettre votre pull request. Si elle ne
fusionne pas proprement avec master, il vous sera peut-être demandé de
rebaser vos modifications.
- Les commits doivent être aussi petits que possible, tout en s'assurant que
chaque commit est correct de manière indépendante (c.-à-d. que chaque commit
doit compiler et passer les tests).
- Les commits doivent être accompagnés d'une signature Developer Certificate of
Origin (http://developercertificate.org), indiquant que vous (et votre
employeur le cas échéant) acceptez d'être liés par les termes de la
[licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de
`git commit`.
- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne
spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une
revue dans la pull request ou un commentaire, ou vous pouvez demander une
revue par [e-mail](mailto:info@rustdesk.com).
- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité.
Pour des instructions git spécifiques, consultez le
[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Conduite
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Communication
Les contributeurs de RustDesk se retrouvent fréquemment sur
[Discord](https://discord.gg/nDceKgxnkV).

View File

@@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/Osx : vcpkg install libvpx libyuv opus aom
- Linux/macOS : vcpkg install libvpx libyuv opus aom
- Exécuter `cargo run`
- Exécutez `cargo run`
## Comment compiler/build sous Linux
@@ -93,7 +93,7 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
Exécution du cargo
cargo run
```
## Comment construire avec Docker

16
docs/SECURITY-FR.md Normal file
View File

@@ -0,0 +1,16 @@
# Politique de sécurité
## Signaler une vulnérabilité
Nous accordons une très grande importance à la sécurité du projet. Nous
encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils
découvrent.
Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez
la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com.
À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite
équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler
toute vulnérabilité de manière responsable afin que nous puissions continuer à
développer une application sécurisée pour l'ensemble de la communauté.

View File

@@ -18,7 +18,7 @@
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
<li> P2P connection with end-to-end encryption based on NaCl. </li>
<li> No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand. </li>
<li> No administrative privileges or installation needed for Windows, elevate privilege locally or from remote on demand. </li>
<li> We like to keep things simple and will strive to make simpler where possible. </li>
</ul>
<p>
@@ -56,4 +56,4 @@
<control>pointing</control>
</supports>
<content_rating type="oars-1.1"/>
</component>
</component>

View File

@@ -62,7 +62,13 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
return false
}
}
audioRecorder = builder.build()
val recorder = try {
builder.build()
} catch (e: Exception) {
Log.e(logTag, "createAudioRecorder failed", e)
return false
}
audioRecorder = recorder
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return true
}

View File

@@ -0,0 +1 @@
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="4" width="19" height="19" fill="#f25022"/><rect x="25" y="4" width="19" height="19" fill="#7fba00"/><rect x="4" y="25" width="19" height="19" fill="#00a4ef"/><rect x="25" y="25" width="19" height="19" fill="#ffb900"/></svg>

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -2365,6 +2365,19 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
id = uri.path.substring("/new/".length);
} else if (uri.authority == "config") {
if (isAndroid || isIOS) {
final allowDeepLinkServerSettings =
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) ==
'Y';
if (!allowDeepLinkServerSettings) {
debugPrint(
"Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled.");
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
// Delay toast to avoid missing overlay during cold-start deeplink handling.
Timer(Duration(seconds: 1), () {
showToast(translate('Failed'));
});
return null;
}
final config = uri.path.substring("/".length);
// add a timer to make showToast work
Timer(Duration(seconds: 1), () {
@@ -2374,6 +2387,18 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
return null;
} else if (uri.authority == "password") {
if (isAndroid || isIOS) {
final allowDeepLinkPassword =
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y';
if (!allowDeepLinkPassword) {
debugPrint(
"Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled.");
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
// Delay toast to avoid missing overlay during cold-start deeplink handling.
Timer(Duration(seconds: 1), () {
showToast(translate('Failed'));
});
return null;
}
final password = uri.path.substring("/".length);
if (password.isNotEmpty) {
Timer(Duration(seconds: 1), () async {

View File

@@ -54,9 +54,9 @@ class _AddressBookState extends State<AddressBook> {
const LinearProgressIndicator(),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPullError,
err: gFFI.abModel.abPullError,
retry: null,
close: () => gFFI.abModel.currentAbPullError.value = ''),
close: gFFI.abModel.clearPullErrors),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPushError,

View File

@@ -0,0 +1,65 @@
// flutter/lib/common/widgets/keyboard_shortcuts/display.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../consts.dart';
import '../../../models/platform_model.dart';
/// Read the bindings JSON and produce a human-readable shortcut string for
/// `actionId`, formatted for the current OS. Returns null if unbound.
class ShortcutDisplay {
static String? formatFor(String actionId) {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null;
final Map<String, dynamic> parsed;
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
return null;
}
if (parsed['enabled'] != true) return null;
final list = (parsed['bindings'] as List? ?? []).cast<Map<String, dynamic>>();
final found = list.firstWhere(
(b) => b['action'] == actionId,
orElse: () => {},
);
if (found.isEmpty) return null;
// Guard against a hand-edited / corrupt config where `key` is missing or
// not a string — silently treat the binding as unbound rather than
// crashing the toolbar render.
final keyValue = found['key'];
if (keyValue is! String) return null;
final isMac = defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
// `mods` similarly may be malformed; treat a non-list as no modifiers.
final modsRaw = found['mods'];
final mods = modsRaw is List
? modsRaw.whereType<String>().toList()
: const <String>[];
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue;
switch (m) {
case 'primary': parts.add(isMac ? '' : 'Ctrl'); break;
case 'alt': parts.add(isMac ? '' : 'Alt'); break;
case 'shift': parts.add(isMac ? '' : 'Shift'); break;
}
}
parts.add(_keyDisplay(keyValue, isMac));
return isMac ? parts.join('') : parts.join('+');
}
static String _keyDisplay(String key, bool isMac) {
switch (key) {
case 'delete': return isMac ? '' : 'Del';
case 'enter': return isMac ? '' : 'Enter';
case 'arrow_left': return '';
case 'arrow_right':return '';
case 'arrow_up': return '';
case 'arrow_down': return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
}

View File

@@ -0,0 +1,490 @@
// flutter/lib/common/widgets/keyboard_shortcuts/page_body.dart
//
// Shared body widget for the Keyboard Shortcuts configuration page. Both the
// desktop (`desktop/pages/desktop_keyboard_shortcuts_page.dart`) and mobile
// (`mobile/pages/mobile_keyboard_shortcuts_page.dart`) pages render this
// widget inside their own platform-styled Scaffold + AppBar shell.
//
// The body owns:
// * the top-level enable/disable toggle (mirrors the General-tab toggle —
// same JSON key, same semantics);
// * a grouped list of actions, each with its current binding plus
// edit / clear icons;
// * the JSON read/write helpers under [kShortcutLocalConfigKey] in the
// canonical {enabled, bindings:[{action,mods,key}]} shape;
// * the recording-dialog round-trip and conflict-replace bookkeeping;
// * "Reset to defaults" (called from the platform AppBar).
//
// Platform shells supply only:
// * the AppBar (with a "Reset to defaults" action that calls
// [KeyboardShortcutsPageBodyState.resetToDefaultsWithConfirm]);
// * surrounding padding / list-tile vs. dense-row visuals via the
// [compact] flag.
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../common.dart';
import '../../../consts.dart';
import '../../../models/platform_model.dart';
import '../../../models/shortcut_model.dart';
import 'recording_dialog.dart';
/// One configurable action — id + i18n key for its label.
class KeyboardShortcutActionEntry {
final String id;
final String labelKey;
const KeyboardShortcutActionEntry(this.id, this.labelKey);
}
/// A named group of actions (e.g. "Session Control").
class KeyboardShortcutActionGroup {
final String titleKey;
final List<KeyboardShortcutActionEntry> actions;
const KeyboardShortcutActionGroup(this.titleKey, this.actions);
}
/// Canonical action group definitions used by both the desktop and mobile
/// configuration pages. The order of groups and entries here is the order
/// the user sees in the UI. (Not `const` because the per-tab ids come from
/// the `kShortcutActionSwitchTab(n)` helper in `consts.dart`.)
final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
KeyboardShortcutActionGroup('Session Control', [
KeyboardShortcutActionEntry(
kShortcutActionSendCtrlAltDel, 'Insert Ctrl + Alt + Del'),
KeyboardShortcutActionEntry(kShortcutActionInsertLock, 'Insert Lock'),
KeyboardShortcutActionEntry(kShortcutActionRefresh, 'Refresh'),
KeyboardShortcutActionEntry(kShortcutActionSwitchSides, 'Switch Sides'),
KeyboardShortcutActionEntry(
kShortcutActionToggleRecording, 'Toggle Recording'),
KeyboardShortcutActionEntry(
kShortcutActionToggleBlockInput, 'Toggle Block User Input'),
]),
KeyboardShortcutActionGroup('Display', [
KeyboardShortcutActionEntry(
kShortcutActionToggleFullscreen, 'Toggle Fullscreen'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayNext, 'Switch to next display'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayPrev, 'Switch to previous display'),
KeyboardShortcutActionEntry(kShortcutActionViewMode1to1, 'View Mode 1:1'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeShrink, 'View Mode Shrink'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeStretch, 'View Mode Stretch'),
]),
KeyboardShortcutActionGroup('Other', [
KeyboardShortcutActionEntry(kShortcutActionScreenshot, 'Take Screenshot'),
KeyboardShortcutActionEntry(kShortcutActionToggleAudio, 'Toggle Audio'),
KeyboardShortcutActionEntry(
kShortcutActionTogglePrivacyMode, 'Toggle Privacy Mode'),
for (var n = 1; n <= 9; n++)
KeyboardShortcutActionEntry(
kShortcutActionSwitchTab(n), 'Switch Tab $n'),
]),
];
/// The shared body widget. Render this inside a platform-styled Scaffold.
///
/// [compact] toggles the desktop dense-row layout (`true`) versus the mobile
/// touch-friendly ListTile layout (`false`).
///
/// [editButtonHint] is shown as the tooltip on the Edit icon. Mobile shells
/// use this to clarify that recording requires a physical keyboard.
///
/// [headerBanner] is an optional widget rendered above the toggle. Mobile
/// uses this to show the "Recording requires a physical keyboard" hint.
class KeyboardShortcutsPageBody extends StatefulWidget {
final bool compact;
final String? editButtonHint;
final Widget? headerBanner;
const KeyboardShortcutsPageBody({
Key? key,
this.compact = true,
this.editButtonHint,
this.headerBanner,
}) : super(key: key);
@override
State<KeyboardShortcutsPageBody> createState() =>
KeyboardShortcutsPageBodyState();
}
/// Public state so platform shells can call [resetToDefaultsWithConfirm] from
/// their AppBar action.
class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
// ----- Persistence helpers -----
Map<String, dynamic> _readJson() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return {'enabled': false, 'bindings': <dynamic>[]};
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
parsed['bindings'] ??= <dynamic>[];
parsed['enabled'] ??= false;
return parsed;
} catch (_) {
return {'enabled': false, 'bindings': <dynamic>[]};
}
}
Future<void> _writeJson(Map<String, dynamic> json) async {
await bind.mainSetLocalOption(
key: kShortcutLocalConfigKey, value: jsonEncode(json));
// Refresh the matcher cache so writes take effect immediately. On native
// this hits the Rust matcher; on Web the bridge forwards to the JS-side
// matcher in flutter/web/js/.
bind.mainReloadKeyboardShortcuts();
if (mounted) setState(() {});
}
/// Replace the bindings entry for [actionId] with [binding]. If [binding]
/// is null, removes the existing entry. If the user is replacing a
/// conflicting binding, [clearActionId] points at the action whose
/// (now-stale) binding should be removed in the same write.
Future<void> _setBinding(
String actionId, {
Map<String, dynamic>? binding,
String? clearActionId,
}) async {
final json = _readJson();
final list = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>()
.toList();
list.removeWhere((b) {
final a = b['action'];
return a == actionId || (clearActionId != null && a == clearActionId);
});
if (binding != null) {
list.add(binding);
}
json['bindings'] = list;
await _writeJson(json);
}
Future<void> _setEnabled(bool v) async {
final json = _readJson();
json['enabled'] = v;
// First-time enable: seed defaults if the user has never bound anything.
final list = (json['bindings'] as List?) ?? const [];
if (v && list.isEmpty) {
json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
}
await _writeJson(json);
}
Future<void> _resetToDefaults() async {
final json = _readJson();
json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
await _writeJson(json);
}
String _labelFor(String actionId) {
for (final g in kKeyboardShortcutActionGroups) {
for (final a in g.actions) {
if (a.id == actionId) return translate(a.labelKey);
}
}
return actionId;
}
// ----- UI handlers -----
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
final json = _readJson();
final bindings = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>();
final result = await showRecordingDialog(
context: context,
actionId: entry.id,
actionLabel: translate(entry.labelKey),
existingBindings: bindings,
actionLabelLookup: _labelFor,
);
if (result == null) return;
await _setBinding(
entry.id,
binding: result.binding,
clearActionId: result.clearActionId,
);
}
Future<void> _onClear(KeyboardShortcutActionEntry entry) async {
await _setBinding(entry.id, binding: null);
}
/// Public — invoked from the platform AppBar action.
Future<void> resetToDefaultsWithConfirm() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(translate('Reset to defaults')),
content: Text(translate('shortcut-reset-confirm-tip')),
actions: [
dialogButton('Cancel',
onPressed: () => Navigator.of(ctx).pop(false),
isOutline: true),
dialogButton('OK', onPressed: () => Navigator.of(ctx).pop(true)),
],
),
);
if (confirmed == true) {
await _resetToDefaults();
}
}
// ----- Build -----
@override
Widget build(BuildContext context) {
final enabled = ShortcutModel.isEnabled();
final theme = Theme.of(context);
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (widget.headerBanner != null) ...[
widget.headerBanner!,
const SizedBox(height: 12),
],
// Top toggle — mirrors the General-tab _OptionCheckBox semantics.
Row(
children: [
Checkbox(
value: enabled,
onChanged: (v) async {
if (v == null) return;
await _setEnabled(v);
},
),
const SizedBox(width: 4),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _setEnabled(!enabled),
child: Text(
translate('Enable keyboard shortcuts in remote session'),
),
),
),
],
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
translate('shortcut-page-description'),
style: TextStyle(color: theme.hintColor),
),
),
const SizedBox(height: 16),
// Disabled visual state when toggle is off — but still scrollable.
Opacity(
opacity: enabled ? 1.0 : 0.5,
child: AbsorbPointer(
absorbing: !enabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final group in kKeyboardShortcutActionGroups)
_buildGroup(context, group),
],
),
),
),
],
);
}
Widget _buildGroup(BuildContext context, KeyboardShortcutActionGroup group) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
Text(
translate(group.titleKey),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 8),
const Expanded(
child: Divider(thickness: 1),
),
],
),
),
const SizedBox(height: 4),
for (final action in group.actions)
widget.compact
? _buildCompactRow(context, action)
: _buildTouchRow(context, action),
],
);
}
/// Desktop dense row: label | shortcut | edit | clear, all in one Row.
Widget _buildCompactRow(
BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
Expanded(
flex: 5,
child: Text(translate(entry.labelKey)),
),
Expanded(
flex: 4,
child: Text(
shortcut ?? '',
style: TextStyle(
fontFamily: defaultTargetPlatform == TargetPlatform.windows
? 'Consolas'
: 'monospace',
color: hasBinding ? null : Theme.of(context).hintColor,
),
),
),
IconButton(
tooltip: widget.editButtonHint ?? translate('Edit'),
onPressed: () => _onEdit(entry),
icon: const Icon(Icons.edit_outlined, size: 18),
),
SizedBox(
width: 40,
child: hasBinding
? IconButton(
tooltip: translate('Clear'),
onPressed: () => _onClear(entry),
icon: const Icon(Icons.close, size: 18),
)
: const SizedBox.shrink(),
),
],
),
);
}
/// Mobile touch row: ListTile with title + subtitle + trailing icons.
Widget _buildTouchRow(
BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null;
return ListTile(
dense: false,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(translate(entry.labelKey)),
subtitle: Text(
shortcut ?? '',
style: TextStyle(
fontFamily: defaultTargetPlatform == TargetPlatform.windows
? 'Consolas'
: 'monospace',
color: hasBinding ? null : Theme.of(context).hintColor,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: widget.editButtonHint ?? translate('Edit'),
onPressed: () => _onEdit(entry),
icon: const Icon(Icons.edit_outlined),
),
if (hasBinding)
IconButton(
tooltip: translate('Clear'),
onPressed: () => _onClear(entry),
icon: const Icon(Icons.close),
)
else
const SizedBox(width: 48),
],
),
);
}
}
/// Thin wrapper around [ShortcutDisplay.formatFor] that ignores the
/// `enabled` flag so the configuration page can always show the user what
/// they have bound, even when the feature is currently disabled.
class ShortcutDisplayForActionId {
static String? format(String actionId) {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null;
final Map<String, dynamic> parsed;
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
return null;
}
final list = (parsed['bindings'] as List? ?? const [])
.cast<Map<String, dynamic>>();
final found = list.firstWhere(
(b) => b['action'] == actionId,
orElse: () => {},
);
if (found.isEmpty) return null;
// Guard against a hand-edited / corrupt config where `key` is missing or
// not a string — render the row as unbound instead of crashing the
// settings page.
final keyValue = found['key'];
if (keyValue is! String) return null;
final isMac = defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
// `mods` similarly may be malformed; treat a non-list as no modifiers.
final modsRaw = found['mods'];
final mods = modsRaw is List
? modsRaw.whereType<String>().toList()
: const <String>[];
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue;
switch (m) {
case 'primary':
parts.add(isMac ? '' : 'Ctrl');
break;
case 'alt':
parts.add(isMac ? '' : 'Alt');
break;
case 'shift':
parts.add(isMac ? '' : 'Shift');
break;
}
}
parts.add(_keyDisplay(keyValue, isMac));
return isMac ? parts.join('') : parts.join('+');
}
static String _keyDisplay(String key, bool isMac) {
switch (key) {
case 'delete':
return isMac ? '' : 'Del';
case 'enter':
return isMac ? '' : 'Enter';
case 'arrow_left':
return '';
case 'arrow_right':
return '';
case 'arrow_up':
return '';
case 'arrow_down':
return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
}

View File

@@ -0,0 +1,371 @@
// flutter/lib/common/widgets/keyboard_shortcuts/recording_dialog.dart
//
// Modal dialog used by the Keyboard Shortcuts settings page to capture a new
// key combination for a given action. The dialog listens for KeyDown events,
// extracts the modifier set + non-modifier key, validates against the
// "must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS)" rule, and reports
// any conflict with another already-bound action.
//
// On Save, returns the new binding map ({action, mods, key}) plus the
// optional id of the action whose binding should be cleared (the conflict
// "Replace" path). On Cancel, returns null.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../common.dart';
/// Result of the recording dialog.
class RecordingResult {
/// The new binding map to write: {action, mods, key}.
final Map<String, dynamic> binding;
/// If the chosen combo conflicted with another action, the user chose
/// "Replace" — the caller must clear this action's binding before writing
/// the new one.
final String? clearActionId;
RecordingResult(this.binding, this.clearActionId);
}
/// Show the recording dialog.
///
/// [actionId] is the action being edited (used for the title and to detect
/// "binding to itself" — that's not a conflict).
/// [actionLabel] is the translated, user-facing action name.
/// [existingBindings] is the current bindings list (used for conflict detection).
/// [actionLabelLookup] resolves an actionId to its translated label, used in
/// the conflict warning.
Future<RecordingResult?> showRecordingDialog({
required BuildContext context,
required String actionId,
required String actionLabel,
required List<Map<String, dynamic>> existingBindings,
required String Function(String) actionLabelLookup,
}) {
return showDialog<RecordingResult>(
context: context,
barrierDismissible: false,
builder: (ctx) => _RecordingDialog(
actionId: actionId,
actionLabel: actionLabel,
existingBindings: existingBindings,
actionLabelLookup: actionLabelLookup,
),
);
}
class _RecordingDialog extends StatefulWidget {
final String actionId;
final String actionLabel;
final List<Map<String, dynamic>> existingBindings;
final String Function(String) actionLabelLookup;
const _RecordingDialog({
required this.actionId,
required this.actionLabel,
required this.existingBindings,
required this.actionLabelLookup,
});
@override
State<_RecordingDialog> createState() => _RecordingDialogState();
}
class _RecordingDialogState extends State<_RecordingDialog> {
final FocusNode _focusNode = FocusNode();
// Captured combo. null until the user presses something with a non-modifier.
Set<String> _mods = {};
String? _key;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
bool get _isMac =>
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
/// True when the captured combo includes the required Ctrl+Alt+Shift
/// (Cmd+Option+Shift on macOS) prefix and a non-modifier key.
bool get _hasRequiredPrefix =>
_mods.contains('primary') &&
_mods.contains('alt') &&
_mods.contains('shift');
/// Return the actionId that this combo currently conflicts with, or null.
/// The action being edited is not a conflict with itself.
String? get _conflictActionId {
if (_key == null || !_hasRequiredPrefix) return null;
for (final b in widget.existingBindings) {
final otherAction = b['action'] as String?;
if (otherAction == null || otherAction == widget.actionId) continue;
final otherKey = b['key'] as String?;
final otherMods =
((b['mods'] as List?) ?? const []).cast<String>().toSet();
if (otherKey == _key &&
otherMods.length == _mods.length &&
otherMods.containsAll(_mods)) {
return otherAction;
}
}
return null;
}
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
return KeyEventResult.handled;
}
if (event is! KeyDownEvent) return KeyEventResult.handled;
// Ignore modifier-only KeyDowns: don't lock in a partial combo.
final logical = event.logicalKey;
final keyName = _logicalToKeyName(logical);
final mods = <String>{};
if (HardwareKeyboard.instance.isAltPressed) mods.add('alt');
if (HardwareKeyboard.instance.isShiftPressed) mods.add('shift');
final primary = _isMac
? HardwareKeyboard.instance.isMetaPressed
: HardwareKeyboard.instance.isControlPressed;
if (primary) mods.add('primary');
setState(() {
_mods = mods;
// Only lock in the key when it's a non-modifier we recognize.
// Modifier-only KeyDowns (Shift, Ctrl, etc.) leave the captured key
// untouched, so the user can adjust modifiers after the fact.
if (keyName != null) {
_key = keyName;
}
});
return KeyEventResult.handled;
}
void _onSave() {
if (_key == null || !_hasRequiredPrefix) return;
// Sort mods to match the canonical order used by Rust default_bindings:
// primary, alt, shift.
final ordered = <String>[
if (_mods.contains('primary')) 'primary',
if (_mods.contains('alt')) 'alt',
if (_mods.contains('shift')) 'shift',
];
final binding = <String, dynamic>{
'action': widget.actionId,
'mods': ordered,
'key': _key!,
};
Navigator.of(context).pop(RecordingResult(binding, _conflictActionId));
}
String _formatPrefix() {
if (_isMac) return 'Cmd+Option+Shift';
return 'Ctrl+Alt+Shift';
}
String _formatCombo() {
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!_mods.contains(m)) continue;
switch (m) {
case 'primary':
parts.add(_isMac ? '' : 'Ctrl');
break;
case 'alt':
parts.add(_isMac ? '' : 'Alt');
break;
case 'shift':
parts.add(_isMac ? '' : 'Shift');
break;
}
}
if (_key != null) {
parts.add(_keyDisplay(_key!));
}
if (parts.isEmpty) return translate('shortcut-recording-press-keys-tip');
return _isMac ? parts.join('') : parts.join('+');
}
String _keyDisplay(String key) {
switch (key) {
case 'delete':
return _isMac ? '' : 'Del';
case 'enter':
return _isMac ? '' : 'Enter';
case 'arrow_left':
return '';
case 'arrow_right':
return '';
case 'arrow_up':
return '';
case 'arrow_down':
return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
@override
Widget build(BuildContext context) {
final hasKey = _key != null;
final conflictId = _conflictActionId;
final hasConflict = conflictId != null;
final canSave = hasKey && _hasRequiredPrefix;
Widget statusLine;
if (!hasKey) {
statusLine = Text(
translate('shortcut-recording-press-keys-tip'),
style: TextStyle(color: Theme.of(context).hintColor),
);
} else if (!_hasRequiredPrefix) {
statusLine = Row(
children: [
Icon(Icons.close, size: 16, color: Colors.red),
const SizedBox(width: 6),
Flexible(
child: Text(
'${translate('shortcut-must-include-prefix')} ${_formatPrefix()}',
style: const TextStyle(color: Colors.red),
),
),
],
);
} else if (hasConflict) {
final otherLabel = widget.actionLabelLookup(conflictId);
statusLine = Row(
children: [
Icon(Icons.warning_amber_outlined,
size: 16, color: Colors.orange.shade700),
const SizedBox(width: 6),
Flexible(
child: Text(
'${translate('shortcut-already-bound-to')} "$otherLabel"',
style: TextStyle(color: Colors.orange.shade700),
),
),
],
);
} else {
statusLine = Row(
children: [
const Icon(Icons.check, size: 16, color: Colors.green),
const SizedBox(width: 6),
Text(translate('Valid'),
style: const TextStyle(color: Colors.green)),
],
);
}
final saveLabel = hasConflict ? 'Replace' : 'Save';
return AlertDialog(
title: Text(
'${translate('Set Shortcut')}: ${widget.actionLabel}',
),
content: Focus(
focusNode: _focusNode,
autofocus: true,
onKeyEvent: _onKeyEvent,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 380),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate('shortcut-recording-instruction')),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 18, horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_formatCombo(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: hasKey
? Theme.of(context).textTheme.titleLarge?.color
: Theme.of(context).hintColor,
),
),
),
const SizedBox(height: 12),
statusLine,
],
),
),
),
actions: [
dialogButton('Cancel',
onPressed: () => Navigator.of(context).pop(),
isOutline: true),
dialogButton(saveLabel, onPressed: canSave ? _onSave : null),
],
);
}
/// Mirror of `event_to_key_name` in `src/keyboard/shortcuts.rs` and
/// `logicalToKeyName` in `flutter/web/js/src/shortcut_matcher.ts` — keep
/// the three in lockstep. Returns null for modifier-only or unsupported keys.
static String? _logicalToKeyName(LogicalKeyboardKey k) {
if (k == LogicalKeyboardKey.delete) return 'delete';
if (k == LogicalKeyboardKey.enter ||
k == LogicalKeyboardKey.numpadEnter) return 'enter';
if (k == LogicalKeyboardKey.arrowLeft) return 'arrow_left';
if (k == LogicalKeyboardKey.arrowRight) return 'arrow_right';
if (k == LogicalKeyboardKey.arrowUp) return 'arrow_up';
if (k == LogicalKeyboardKey.arrowDown) return 'arrow_down';
final letters = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.keyA: 'a', LogicalKeyboardKey.keyB: 'b',
LogicalKeyboardKey.keyC: 'c', LogicalKeyboardKey.keyD: 'd',
LogicalKeyboardKey.keyE: 'e', LogicalKeyboardKey.keyF: 'f',
LogicalKeyboardKey.keyG: 'g', LogicalKeyboardKey.keyH: 'h',
LogicalKeyboardKey.keyI: 'i', LogicalKeyboardKey.keyJ: 'j',
LogicalKeyboardKey.keyK: 'k', LogicalKeyboardKey.keyL: 'l',
LogicalKeyboardKey.keyM: 'm', LogicalKeyboardKey.keyN: 'n',
LogicalKeyboardKey.keyO: 'o', LogicalKeyboardKey.keyP: 'p',
LogicalKeyboardKey.keyQ: 'q', LogicalKeyboardKey.keyR: 'r',
LogicalKeyboardKey.keyS: 's', LogicalKeyboardKey.keyT: 't',
LogicalKeyboardKey.keyU: 'u', LogicalKeyboardKey.keyV: 'v',
LogicalKeyboardKey.keyW: 'w', LogicalKeyboardKey.keyX: 'x',
LogicalKeyboardKey.keyY: 'y', LogicalKeyboardKey.keyZ: 'z',
};
if (letters.containsKey(k)) return letters[k];
final digits = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.digit1: 'digit1',
LogicalKeyboardKey.digit2: 'digit2',
LogicalKeyboardKey.digit3: 'digit3',
LogicalKeyboardKey.digit4: 'digit4',
LogicalKeyboardKey.digit5: 'digit5',
LogicalKeyboardKey.digit6: 'digit6',
LogicalKeyboardKey.digit7: 'digit7',
LogicalKeyboardKey.digit8: 'digit8',
LogicalKeyboardKey.digit9: 'digit9',
};
if (digits.containsKey(k)) return digits[k];
return null;
}
}

View File

@@ -20,7 +20,8 @@ const kOpSvgList = [
'okta',
'facebook',
'azure',
'auth0'
'auth0',
'microsoft'
];
class _IconOP extends StatelessWidget {
@@ -224,21 +225,59 @@ class _WidgetOPState extends State<WidgetOP> {
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: RichText(
text: TextSpan(
text: '$_stateMsg ',
style:
DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
children: <TextSpan>[
TextSpan(
text: _failedMsg,
style: DefaultTextStyle.of(context).style.copyWith(
fontSize: 14,
color: Colors.red,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (_stateMsg.isNotEmpty && _failedMsg.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SelectableText(
translate(_stateMsg),
style: DefaultTextStyle.of(context)
.style
.copyWith(fontSize: 12),
),
),
],
),
if (_failedMsg.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Builder(builder: (context) {
final errorColor =
Theme.of(context).colorScheme.error;
final bgColor = Theme.of(context)
.colorScheme
.errorContainer
.withOpacity(0.3);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
color: errorColor, size: 16),
const SizedBox(width: 6),
Flexible(
child: SelectableText(
translate(_failedMsg),
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 13,
color: errorColor,
),
),
),
],
),
);
}),
),
],
),
);
}),

View File

@@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are seperated key events for en-US input method.
// while `Alt` and `Control` are separated key events for en-US input method.
return FocusScope(
autofocus: true,
child: Focus(
@@ -532,7 +532,9 @@ class _RawTouchGestureDetectorRegionState
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), (instance) {
() => TapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
@@ -540,14 +542,18 @@ class _RawTouchGestureDetectorRegionState
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(), (instance) {
() => DoubleTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap;
}),
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(), (instance) {
() => LongPressGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
@@ -557,7 +563,9 @@ class _RawTouchGestureDetectorRegionState
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer(),
() => HoldTapMoveGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
),
(instance) => instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
@@ -565,14 +573,18 @@ class _RawTouchGestureDetectorRegionState
..onHoldDragEnd = onHoldDragEnd),
DoubleFinerTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer(), (instance) {
() => DoubleFinerTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance
..onDoubleFinerTap = onDoubleFinerTap
..onDoubleFinerTapDown = onDoubleFinerTapDown;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer(), (instance) {
() => CustomTouchGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance.onOneFingerPanStart =
(DragStartDetails d) => onOneFingerPanStart(context, d);
instance

View File

@@ -21,11 +21,13 @@ class TTextMenu {
final VoidCallback? onPressed;
Widget? trailingIcon;
bool divider;
final String? actionId;
TTextMenu(
{required this.child,
required this.onPressed,
this.trailingIcon,
this.divider = false});
this.divider = false,
this.actionId});
Widget getChild() {
if (trailingIcon != null) {
@@ -229,7 +231,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(
TTextMenu(
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId),
actionId: kShortcutActionSendCtrlAltDel),
);
}
// restart
@@ -250,7 +253,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(
TTextMenu(
child: Text(translate('Insert Lock')),
onPressed: () => bind.sessionLockScreen(sessionId: sessionId)),
onPressed: () => bind.sessionLockScreen(sessionId: sessionId),
actionId: kShortcutActionInsertLock),
);
}
// blockUserInput
@@ -268,26 +272,28 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
sessionId: sessionId,
value: '${blockInput.value ? 'un' : ''}block-input');
blockInput.value = !blockInput.value;
}));
},
actionId: kShortcutActionToggleBlockInput));
}
// switchSides
if (isDefaultConn &&
isDesktop &&
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu(
child: Text(translate('Switch Sides')),
onPressed: () =>
showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager)));
showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager),
actionId: kShortcutActionSwitchSides));
}
// refresh
if (pi.version.isNotEmpty) {
v.add(TTextMenu(
child: Text(translate('Refresh')),
onPressed: () => sessionRefreshVideo(sessionId, pi),
actionId: kShortcutActionRefresh,
));
}
// record
@@ -309,7 +315,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
)
],
),
onPressed: () => ffi.recordingModel.toggle()));
onPressed: () => ffi.recordingModel.toggle(),
actionId: kShortcutActionToggleRecording));
}
// to-do:
@@ -343,6 +350,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
});
}
},
actionId: kShortcutActionScreenshot,
));
}
}
@@ -353,6 +361,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
));
}
// Register tagged callbacks with the shortcut model so global keyboard
// shortcuts can dispatch the same actions as the toolbar menu items.
for (final menu in v) {
if (menu.actionId != null && menu.onPressed != null) {
ffi.shortcutModel.register(menu.actionId!, menu.onPressed!);
}
}
return v;
}

View File

@@ -187,6 +187,9 @@ const String kOptionDisableChangeId = "disable-change-id";
const String kOptionDisableUnlockPin = "disable-unlock-pin";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionAllowDeepLinkPassword = "allow-deep-link-password";
const String kOptionAllowDeepLinkServerSettings =
"allow-deep-link-server-settings";
const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";
@@ -683,3 +686,24 @@ extension WindowsTargetExt on int {
}
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
// Keyboard shortcut Action IDs - must match src/keyboard/shortcuts.rs::action_id.
const kShortcutActionSendCtrlAltDel = 'send_ctrl_alt_del';
const kShortcutActionToggleFullscreen = 'toggle_fullscreen';
const kShortcutActionSwitchDisplayNext = 'switch_display_next';
const kShortcutActionSwitchDisplayPrev = 'switch_display_prev';
const kShortcutActionScreenshot = 'screenshot';
const kShortcutActionInsertLock = 'insert_lock';
const kShortcutActionRefresh = 'refresh';
const kShortcutActionToggleAudio = 'toggle_audio';
const kShortcutActionToggleBlockInput = 'toggle_block_input';
const kShortcutActionToggleRecording = 'toggle_recording';
const kShortcutActionTogglePrivacyMode = 'toggle_privacy_mode';
const kShortcutActionViewMode1to1 = 'view_mode_1_to_1';
const kShortcutActionViewModeShrink = 'view_mode_shrink';
const kShortcutActionViewModeStretch = 'view_mode_stretch';
const kShortcutActionSwitchSides = 'switch_sides';
String kShortcutActionSwitchTab(int n) => 'switch_tab_$n';
const kShortcutLocalConfigKey = 'keyboard-shortcuts';
const kShortcutEventName = 'shortcut_triggered';

View File

@@ -0,0 +1,58 @@
// flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart
//
// Desktop shell for the Keyboard Shortcuts configuration page. Users land
// here from the General settings tab. The page exposes:
// * A top-level enable/disable toggle (mirrors the General-tab toggle —
// same JSON key, same semantics).
// * A grouped, scrollable list of actions, each with a current binding and
// edit / clear icons.
// * An AppBar "Reset to defaults" action with a confirmation dialog.
//
// All edits write back to LocalConfig under [kShortcutLocalConfigKey] in the
// canonical {enabled, bindings:[{action,mods,key}]} shape that the Rust and
// Web matchers consume.
//
// The body — group definitions, JSON I/O, conflict-replace flow,
// recording-dialog round-trip — lives in
// `common/widgets/keyboard_shortcuts/page_body.dart` and is shared with the
// mobile shell at `mobile/pages/mobile_keyboard_shortcuts_page.dart`.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../common/widgets/keyboard_shortcuts/page_body.dart';
class DesktopKeyboardShortcutsPage extends StatefulWidget {
const DesktopKeyboardShortcutsPage({Key? key}) : super(key: key);
@override
State<DesktopKeyboardShortcutsPage> createState() =>
_DesktopKeyboardShortcutsPageState();
}
class _DesktopKeyboardShortcutsPageState
extends State<DesktopKeyboardShortcutsPage> {
final GlobalKey<KeyboardShortcutsPageBodyState> _bodyKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(translate('Keyboard Shortcuts')),
actions: [
TextButton.icon(
onPressed: () =>
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
icon: const Icon(Icons.restore),
label: Text(translate('Reset to defaults')),
).marginOnly(right: 12),
],
),
body: KeyboardShortcutsPageBody(
key: _bodyKey,
compact: true,
),
);
}
}

View File

@@ -10,12 +10,14 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_keyboard_shortcuts_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/shortcut_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
@@ -421,11 +423,57 @@ class _GeneralState extends State<_General> {
if (!isWeb) audio(context),
if (!isWeb) record(context),
if (!isWeb) WaylandCard(),
other()
other(),
if (!bind.isIncomingOnly()) keyboardShortcuts(),
],
).marginOnly(bottom: _kListViewBottomMargin);
}
Widget keyboardShortcuts() {
// The bindings JSON (LocalConfig key `keyboard-shortcuts`) is the single
// source of truth — it embeds an `enabled` boolean alongside the bindings
// list. We mutate the JSON in place via _OptionCheckBox's optGetter /
// optSetter hooks rather than introducing a parallel boolean key, so the
// Rust matcher and the Web matcher both read the same flag without drift.
return _Card(title: 'Keyboard Shortcuts', children: [
_OptionCheckBox(
context,
'Enable keyboard shortcuts in remote session',
kShortcutLocalConfigKey,
isServer: false,
optGetter: ShortcutModel.isEnabled,
optSetter: (k, v) async {
final raw = bind.mainGetLocalOption(key: k);
Map<String, dynamic> parsed = {};
if (raw.isNotEmpty) {
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
parsed = {};
}
}
parsed['enabled'] = v;
parsed['bindings'] ??= <dynamic>[];
// Seed defaults the first time the user enables shortcuts so the
// common combos (Ctrl+Alt+Shift+Enter for fullscreen, etc.) work
// out of the box. Mirrors the same logic on the dedicated config
// page.
final list = (parsed['bindings'] as List?) ?? const [];
if (v && list.isEmpty) {
parsed['bindings'] =
jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
}
await bind.mainSetLocalOption(key: k, value: jsonEncode(parsed));
// Refresh the matcher cache so the new flag / bindings take effect
// immediately. On native this hits the Rust matcher; on Web the
// bridge forwards to the JS-side matcher in flutter/web/js/.
bind.mainReloadKeyboardShortcuts();
},
),
_ShortcutsConfigureRow(),
]);
}
Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString();
onChanged(String value) async {
@@ -2946,6 +2994,37 @@ class _CountDownButtonState extends State<_CountDownButton> {
}
}
// Tappable row that pushes the shortcut configuration page.
class _ShortcutsConfigureRow extends StatelessWidget {
// ignore: unused_element
const _ShortcutsConfigureRow({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const DesktopKeyboardShortcutsPage(),
));
},
child: Row(
children: [
Expanded(
child: Text(translate('Configure shortcuts...')),
),
Icon(Icons.arrow_forward_ios,
size: 16, color: disabledTextColor(context, true))
.marginOnly(right: 4),
],
).marginOnly(
left: _kCheckBoxLeftMargin,
top: 6,
bottom: 6,
),
);
}
}
//#endregion
//#region dialogs

View File

@@ -17,6 +17,7 @@ import '../../common/widgets/toolbar.dart';
import '../../models/model.dart';
import '../../models/input_model.dart';
import '../../models/platform_model.dart';
import '../../models/shortcut_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
@@ -126,6 +127,19 @@ class _RemotePageState extends State<RemotePage>
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
// Seed shortcut action callbacks once the session is ready, so that
// global keyboard shortcuts work even if the user never opens the
// toolbar menu. The returned list is intentionally discarded — the
// side effect of registering callbacks (inside toolbarControls) is
// what we want here.
if (mounted) {
toolbarControls(context, widget.id, _ffi);
// Register the default-bound actions that `toolbarControls` doesn't
// own (fullscreen, switch display, switch tab). Done in addition,
// not instead of, the toolbar registration above.
registerSessionShortcutActions(_ffi,
tabController: widget.tabController);
}
});
_ffi.canvasModel.initializeEdgeScrollFallback(this);
_ffi.start(

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/display.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -763,8 +764,31 @@ class _ControlMenu extends StatelessWidget {
if (e.divider) {
return Divider();
} else {
final hint = e.actionId == null
? null
: ShortcutDisplay.formatFor(e.actionId!);
final child = hint == null
? e.child
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: e.child),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
hint,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(context).hintColor,
),
),
),
],
);
return MenuButton(
child: e.child,
child: child,
onPressed: e.onPressed,
ffi: ffi,
trailingIcon: e.trailingIcon);

View File

@@ -0,0 +1,95 @@
// flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
//
// Mobile shell for the Keyboard Shortcuts configuration page. Mirrors
// `desktop/pages/desktop_keyboard_shortcuts_page.dart` but with a touch-
// friendly layout (ListTile rows instead of dense rows) and a hint banner
// that explains the recording flow only works with a physical keyboard.
//
// All actual logic — group definitions, JSON I/O, conflict-replace flow,
// recording-dialog round-trip, "Reset to defaults" — lives in the shared
// `common/widgets/keyboard_shortcuts/page_body.dart`. This file only
// supplies the AppBar, the AppBar action, and the platform hint banner.
//
// Mobile keyboard detection limitation: Flutter has no reliable
// "is a physical keyboard attached?" API on iOS or Android. Soft keyboards
// don't generate the `KeyDownEvent`s the recording dialog listens for, so
// in practice the dialog only does anything useful when the user actually
// has a hardware keyboard plugged in (USB / Bluetooth / Smart Connector).
// For V1 we don't try to detect attachment — we just surface the
// requirement as an in-page hint instead of disabling the Edit button.
import 'package:flutter/material.dart';
import '../../common.dart';
import '../../common/widgets/keyboard_shortcuts/page_body.dart';
class MobileKeyboardShortcutsPage extends StatefulWidget {
const MobileKeyboardShortcutsPage({Key? key}) : super(key: key);
@override
State<MobileKeyboardShortcutsPage> createState() =>
_MobileKeyboardShortcutsPageState();
}
class _MobileKeyboardShortcutsPageState
extends State<MobileKeyboardShortcutsPage> {
final GlobalKey<KeyboardShortcutsPageBodyState> _bodyKey = GlobalKey();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(translate('Keyboard Shortcuts')),
actions: [
IconButton(
tooltip: translate('Reset to defaults'),
onPressed: () =>
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
icon: const Icon(Icons.restore),
),
],
),
body: KeyboardShortcutsPageBody(
key: _bodyKey,
compact: false,
editButtonHint: translate('shortcut-mobile-physical-keyboard-tip'),
headerBanner: _PhysicalKeyboardHintBanner(theme: theme),
),
);
}
}
/// A muted info banner shown above the master toggle on mobile. We can't
/// reliably detect whether a physical keyboard is attached, so instead of
/// disabling the Edit button we surface the requirement up front.
class _PhysicalKeyboardHintBanner extends StatelessWidget {
final ThemeData theme;
const _PhysicalKeyboardHintBanner({required this.theme});
@override
Widget build(BuildContext context) {
final color = theme.colorScheme.primary.withOpacity(0.08);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline,
size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
translate('shortcut-mobile-physical-keyboard-tip'),
style: TextStyle(color: theme.colorScheme.onSurface),
),
),
],
),
);
}
}

View File

@@ -21,6 +21,7 @@ import '../../common/widgets/remote_input.dart';
import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/shortcut_model.dart';
import '../../utils/image.dart';
import '../widgets/dialog.dart';
import '../widgets/custom_scale_widget.dart';
@@ -119,6 +120,18 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
_disableAndroidSoftKeyboard(
isKeyboardVisible: keyboardVisibilityController.isVisible);
// Seed shortcut action callbacks once the session is ready, so that
// global keyboard shortcuts work even if the user never opens the
// toolbar menu. The returned list is intentionally discarded — the
// side effect of registering callbacks (inside toolbarControls) is
// what we want here.
if (mounted) {
toolbarControls(context, widget.id, gFFI);
// Mobile has no DesktopTabController, so tab-switch shortcuts
// remain unregistered (they will simply log a no-handler debug
// line if a mobile user binds one — they have no tabs to switch).
registerSessionShortcutActions(gFFI);
}
});
WidgetsBinding.instance.addObserver(this);
}
@@ -426,12 +439,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
return Container(
color: MyTheme.canvasColor,
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
);
}),
),

View File

@@ -17,8 +17,10 @@ import '../../common/widgets/login.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/shortcut_model.dart';
import '../widgets/dialog.dart';
import 'home_page.dart';
import 'mobile_keyboard_shortcuts_page.dart';
import 'scan_page.dart';
class SettingsPage extends StatefulWidget implements PageShape {
@@ -819,6 +821,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
showThemeSettings(gFFI.dialogManager);
},
),
SettingsTile.navigation(
leading: Icon(Icons.keyboard_outlined),
title: Text(translate('Keyboard Shortcuts')),
description: Text(ShortcutModel.isEnabled()
? translate('On')
: translate('Off')),
onPressed: (context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const MobileKeyboardShortcutsPage(),
)).then((_) {
if (mounted) setState(() {});
});
},
),
if (!bind.isDisableAccount())
SettingsTile.switchTile(
title: Text(translate('note-at-conn-end-tip')),
@@ -1352,3 +1370,4 @@ SettingsTile _getPopupDialogRadioEntry({
),
);
}

View File

@@ -259,13 +259,11 @@ class _ViewCameraPageState extends State<ViewCameraPage>
}
return Container(
color: MyTheme.canvasColor,
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
);
}),
),

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
@@ -53,7 +52,9 @@ class AbModel {
RxBool get currentAbLoading => current.abLoading;
bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty;
RxString get currentAbPullError => current.pullError;
final _listPullError = ''.obs;
RxString get abPullError =>
_listPullError.value.isNotEmpty ? _listPullError : current.pullError;
RxString get currentAbPushError => current.pushError;
String? _personalAbGuid;
RxBool legacyMode = false.obs;
@@ -68,6 +69,7 @@ class AbModel {
var _syncFromRecentLock = false;
var _timerCounter = 0;
var _cacheLoadOnceFlag = false;
var _pulledOnce = false;
var listInitialized = false;
var _maxPeerOneAb = 0;
@@ -97,10 +99,17 @@ class AbModel {
print("reset ab model");
addressbooks.clear();
_currentName.value = '';
_listPullError.value = '';
_pulledOnce = false;
await bind.mainClearAb();
listInitialized = false;
}
void clearPullErrors() {
_listPullError.value = '';
current.pullError.value = '';
}
// #region ab
/// Pulls the address book data from the server.
///
@@ -110,31 +119,41 @@ class AbModel {
var _pulling = false;
Future<void> pullAb(
{required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return;
if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (_pulling) return;
if (force == null && _pulledOnce) {
return;
}
_pulling = true;
if (!quiet) {
_listPullError.value = '';
current.pullError.value = '';
}
try {
await _pullAb(force: force, quiet: quiet);
_refreshTab();
} catch (_) {}
_pulling = false;
_pulledOnce = true;
}
Future<void> _pullAb(
{required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return;
if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try {
// Read personal guid every time to avoid upgrading the server without closing the main window
_personalAbGuid = null;
await _getPersonalAbGuid();
// Determine legacy mode based on whether _personalAbGuid is null
// `true`: continue init. `false`: stop, error already recorded.
if (!await _getPersonalAbGuid(quiet: quiet)) {
return;
}
legacyMode.value = _personalAbGuid == null;
if (!legacyMode.value && _maxPeerOneAb == 0) {
await _getAbSettings();
await _getAbSettings(quiet: quiet);
}
if (_personalAbGuid != null) {
debugPrint("pull ab list");
@@ -142,7 +161,7 @@ class AbModel {
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
// get all address book name
await _getSharedAbProfiles(abProfiles);
await _getSharedAbProfiles(abProfiles, quiet: quiet);
addressbooks.removeWhere((key, value) =>
abProfiles.firstWhereOrNull((e) => e.name == key) == null);
for (int i = 0; i < abProfiles.length; i++) {
@@ -182,6 +201,7 @@ class AbModel {
}
} catch (e) {
debugPrint("pull ab list error: $e");
_setListPullError(e, quiet: quiet);
}
} else if (listInitialized &&
(!current.initialized || force == ForcePullAb.current)) {
@@ -197,14 +217,26 @@ class AbModel {
}
}
Future<bool> _getAbSettings() async {
void _setListPullError(Object err, {required bool quiet, int? statusCode}) {
if (!quiet) {
_listPullError.value =
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
}
if (statusCode == 401) {
gFFI.userModel.reset(resetOther: true);
}
}
Future<bool> _getAbSettings({required bool quiet}) async {
int? statusCode;
try {
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
if (resp.statusCode == 404) {
statusCode = resp.statusCode;
if (statusCode == 404) {
debugPrint("HTTP 404, api server doesn't support shared address book");
return false;
}
@@ -213,46 +245,57 @@ class AbModel {
if (json.containsKey('error')) {
throw json['error'];
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
if (statusCode != 200) {
throw 'HTTP $statusCode';
}
_maxPeerOneAb = json['max_peer_one_ab'] ?? 0;
return true;
} catch (err) {
debugPrint('get ab settings err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}
Future<bool> _getPersonalAbGuid() async {
/// Loads `/api/ab/personal`.
/// Returns `true` to continue init, `false` to stop after a real error.
Future<bool> _getPersonalAbGuid({required bool quiet}) async {
int? statusCode;
try {
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
if (resp.statusCode == 404) {
statusCode = resp.statusCode;
if (statusCode == 404) {
debugPrint("HTTP 404, current api server is legacy mode");
return false;
// Old server: keep `_personalAbGuid` null and continue in legacy mode.
return true;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
if (statusCode != 200) {
throw 'HTTP $statusCode';
}
_personalAbGuid = json['guid'];
// New server: guid is available, continue in non-legacy mode.
return true;
} catch (err) {
debugPrint('get personal ab err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
// Real error: stop the current pull.
return false;
}
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles) async {
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles,
{required bool quiet}) async {
final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles";
int? statusCode;
try {
var uri0 = Uri.parse(api);
final pageSize = 100;
@@ -273,13 +316,19 @@ class AbModel {
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(uri, headers: headers);
statusCode = resp.statusCode;
if (statusCode == 404) {
debugPrint(
"HTTP 404, api server doesn't support shared address book");
return false;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
if (statusCode != 200) {
throw 'HTTP $statusCode';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
@@ -302,6 +351,7 @@ class AbModel {
return true;
} catch (err) {
debugPrint('_getSharedAbProfiles err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}

View File

@@ -343,6 +343,7 @@ class GroupModel {
}
reset() async {
initialized = false;
groupLoadError.value = '';
deviceGroups.clear();
users.clear();

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -15,12 +16,13 @@ import 'package:get/get.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'input_modifier_utils.dart';
import 'relative_mouse_model.dart';
import '../common.dart';
import '../consts.dart';
/// Mouse button enum.
enum MouseButtons { left, right, wheel, back }
enum MouseButtons { left, right, wheel, back, forward }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
@@ -157,6 +159,8 @@ extension ToString on MouseButtons {
return 'wheel';
case MouseButtons.back:
return 'back';
case MouseButtons.forward:
return 'forward';
}
}
}
@@ -327,6 +331,80 @@ class ToReleaseKeys {
}
class InputModel {
// Side mouse button support for Linux.
// Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
// natively via GDK and forward through the platform channel.
static InputModel? _activeSideButtonModel;
// Tracks per-button which model received a side button down event, so the
// matching up event is routed there even if the pointer has left the view
// or a different button was pressed in between.
static final Map<MouseButtons, InputModel> _sideButtonDownModels = {};
static bool _sideButtonChannelInitialized = false;
/// Each Flutter engine (main window + sub-windows from desktop_multi_window)
/// runs its own Dart isolate with its own statics. Called from initEnv()
/// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels.
static void initSideButtonChannel() {
if (!Platform.isLinux) return;
if (_sideButtonChannelInitialized) return;
_sideButtonChannelInitialized = true;
const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
channel.setMethodCallHandler((call) async {
if (call.method == 'onSideMouseButton') {
final args = call.arguments as Map<dynamic, dynamic>;
final button = args['button'] as String;
final type = args['type'] as String;
final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
if (type == 'down') {
final model = _activeSideButtonModel;
if (model != null &&
!(model.isViewOnly && !model.showMyCursor) &&
model.keyboardPerm &&
!model.isViewCamera) {
_sideButtonDownModels[mb] = model;
// Fire-and-forget to avoid blocking the platform channel handler.
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
}));
}
} else {
// Only route 'up' when we recorded the matching 'down';
// dropping avoids sending unpaired 'up' to an unrelated session.
// Use _sendMouseUnchecked to bypass permission checks so the
// release always goes through even if permissions changed.
final model = _sideButtonDownModels.remove(mb);
if (model != null) {
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
}));
}
}
}
return null;
});
}
/// Clear any static references to this model (prevents stale routing).
/// Releases any held side buttons on the peer so closing a session
/// mid-press does not leave a stuck button.
void disposeSideButtonTracking() {
if (_activeSideButtonModel == this) _activeSideButtonModel = null;
final held = _sideButtonDownModels.entries
.where((e) => e.value == this)
.map((e) => e.key)
.toList();
for (final mb in held) {
_sideButtonDownModels.remove(mb);
// Best-effort release; session may already be tearing down.
unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) {
debugPrint('[InputModel] failed to release side button $mb: $e');
}));
}
}
final WeakReference<FFI> parent;
String keyboardMode = '';
@@ -412,6 +490,7 @@ class InputModel {
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
InputModel(this.parent) {
initSideButtonChannel();
sessionId = parent.target!.sessionId;
_relativeMouse = RelativeMouseModel(
sessionId: sessionId,
@@ -620,6 +699,39 @@ class InputModel {
}
}
<<<<<<< HEAD
// Safe: this only re-dispatches synthesized Shift key-up events.
// The key-up path clears the tracked Shift state so this does not loop.
void _releaseTrackedShiftKeyEventIfNeeded() {
final leftShift = toReleaseKeys.lastLShiftKeyEvent;
final rightShift = toReleaseKeys.lastRShiftKeyEvent;
if (leftShift != null) {
handleKeyEvent(leftShift);
}
if (rightShift != null) {
handleKeyEvent(rightShift);
}
}
// Safe: this only re-dispatches synthesized Shift key-up events.
// The raw key-up path clears the tracked Shift state so this does not loop.
void _releaseTrackedRawShiftKeyEventIfNeeded() {
final leftShift = toReleaseRawKeys.lastLShiftKeyEvent;
final rightShift = toReleaseRawKeys.lastRShiftKeyEvent;
if (leftShift != null) {
handleRawKeyEvent(RawKeyUpEvent(
data: leftShift.data,
character: leftShift.character,
));
}
if (rightShift != null) {
handleRawKeyEvent(RawKeyUpEvent(
data: rightShift.data,
character: rightShift.character,
));
}
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
@@ -674,6 +786,27 @@ class InputModel {
toReleaseRawKeys.updateKeyUp(key, e);
}
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
// set even though the current raw key event is not shifted anymore.
if (e is RawKeyDownEvent &&
shouldReleaseStaleMobileShift(
isMobile: isMobile,
cachedShiftPressed: shift,
actualShiftPressed: e.isShiftPressed,
logicalKey: e.logicalKey,
hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null ||
toReleaseRawKeys.lastRShiftKeyEvent != null,
)) {
if (kDebugMode) {
debugPrint(
'input: releasing stale mobile Shift before replaying tracked raw '
'key-up (logicalKey=${e.logicalKey.keyLabel}, '
'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)',
);
}
_releaseTrackedRawShiftKeyEventIfNeeded();
}
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e, iosCapsLock);
@@ -694,6 +827,7 @@ class InputModel {
return KeyEventResult.ignored;
}
}
if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
@@ -717,6 +851,8 @@ class InputModel {
iosCapsLock = _getIosCapsFromCharacter(e);
}
// Update cached modifier state before sending the event. The stale mobile
// Shift release check below relies on this cached state.
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
@@ -754,6 +890,21 @@ class InputModel {
}
}
}
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
// set even though the current key event is not shifted anymore.
if (e is KeyDownEvent &&
shouldReleaseStaleMobileShift(
isMobile: isMobile,
cachedShiftPressed: shift,
actualShiftPressed: HardwareKeyboard.instance.isShiftPressed,
logicalKey: e.logicalKey,
hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null ||
toReleaseKeys.lastRShiftKeyEvent != null,
)) {
_releaseTrackedShiftKeyEventIfNeeded();
}
final isDesktopAndMapMode =
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
if (isMobileAndMapMode || isDesktopAndMapMode) {
@@ -966,13 +1117,20 @@ class InputModel {
return evt;
}
/// Send mouse event unconditionally (no permission checks).
/// Used for side button releases that must go through even if permissions
/// changed after the matching down was sent.
Future<void> _sendMouseUnchecked(String type, MouseButtons button) async {
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
}
/// Send mouse press event.
Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return;
if (isViewCamera) return;
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
await _sendMouseUnchecked(type, button);
}
void enterOrLeave(bool enter) {
@@ -982,6 +1140,13 @@ class InputModel {
_pointerInsideImage = enter;
_lastWheelTsUs = 0;
// Track active model for side button events (Linux).
if (enter) {
_activeSideButtonModel = this;
} else if (_activeSideButtonModel == this) {
_activeSideButtonModel = null;
}
// Fix status
if (!enter) {
resetModifiers();
@@ -1332,6 +1497,16 @@ class InputModel {
return false;
}
/// iOS may emit a synthesized touch event after a real mouse click.
/// This helper ignores touch-down events that arrive shortly after a mouse down,
/// even when the position is far (e.g., near the top edge).
bool _shouldIgnoreTouchAfterMouse(int nowMs) {
if (!isIOS) return false;
const int kTouchAfterMouseWindowMs = 700;
final dt = nowMs - _lastMouseDownTimeMs;
return dt >= 0 && dt < kTouchAfterMouseWindowMs;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1344,6 +1519,9 @@ class InputModel {
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true;
}
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
@@ -1353,6 +1531,10 @@ class InputModel {
}
if (e.kind != ui.PointerDeviceKind.mouse) {
// Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue).
if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) {
return;
}
if (isPhysicalMouse.value) {
isPhysicalMouse.value = false;
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/services.dart';
/// Returns true when a stale mobile one-shot Shift state should be released
/// by replaying a tracked Shift key-down as a synthesized key-up.
///
/// This is only valid on mobile when Flutter's cached Shift state is still on
/// (`cachedShiftPressed == true`) but the current hardware/raw event reports
/// Shift as off (`actualShiftPressed == false`).
///
/// A tracked Shift key-down is required so the caller can safely synthesize the
/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the
/// Shift key event itself must be processed first; otherwise we could release
/// the tracked key while still handling the original Shift press/release.
/// Callers should evaluate this only after their cached modifier state has been
/// updated for the current event.
///
/// When this returns true, the caller logs a line like:
/// `input: releasing stale mobile Shift before replaying tracked raw key-up`
/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`.
bool shouldReleaseStaleMobileShift({
required bool isMobile,
required bool cachedShiftPressed,
required bool actualShiftPressed,
required LogicalKeyboardKey logicalKey,
required bool hasTrackedShiftKeyDown,
}) {
if (!isMobile || !cachedShiftPressed || actualShiftPressed) {
return false;
}
if (!hasTrackedShiftKeyDown) {
return false;
}
if (logicalKey == LogicalKeyboardKey.shiftLeft ||
logicalKey == LogicalKeyboardKey.shiftRight) {
return false;
}
return true;
}

View File

@@ -21,6 +21,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/shortcut_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
@@ -476,6 +477,11 @@ class FfiModel with ChangeNotifier {
} else if (name == 'exit_relative_mouse_mode') {
// Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS)
parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease();
} else if (name == kShortcutEventName) {
final action = evt['action'];
if (action is String) {
parent.target?.shortcutModel.onTriggered(action);
}
} else {
debugPrint('Event is not handled in the fixed branch: $name');
}
@@ -3623,6 +3629,7 @@ class FFI {
late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm
late final TextureModel textureModel; //session
late final ShortcutModel shortcutModel; // session
late final Peers recentPeersModel; // global
late final Peers favoritePeersModel; // global
late final Peers lanPeersModel; // global
@@ -3652,6 +3659,7 @@ class FFI {
elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this));
textureModel = TextureModel(WeakReference(this));
shortcutModel = ShortcutModel(WeakReference(this));
recentPeersModel = Peers(
name: PeersModelName.recent,
loadEvent: LoadEvent.recent,
@@ -3932,6 +3940,7 @@ class FFI {
inputModel.resetModifiers();
// Dispose relative mouse mode resources to ensure cursor is restored
inputModel.disposeRelativeMouseMode();
inputModel.disposeSideButtonTracking();
if (closeSession) {
await bind.sessionClose(sessionId: sessionId);
}

View File

@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../common.dart';
import '../consts.dart';
import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController;
import '../models/model.dart';
import '../models/platform_model.dart';
import '../models/state_model.dart';
/// Per-session shortcut dispatcher. Attached to FFI when a session is created.
///
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
/// session events containing the matched `action` id. The session event
/// listener in [FfiModel.startEventListener] forwards those to this model
/// via [onTriggered], which runs whatever callback the toolbar / menu
/// builders previously registered for that action id.
class ShortcutModel {
final WeakReference<FFI> parent;
final Map<String, VoidCallback> _callbacks = {};
ShortcutModel(this.parent);
/// Called by toolbar / menu builders to register what to do when the
/// matched shortcut fires.
void register(String actionId, VoidCallback callback) {
_callbacks[actionId] = callback;
}
void unregister(String actionId) {
_callbacks.remove(actionId);
}
/// Called by the session event listener when a `shortcut_triggered` event
/// arrives for this session.
void onTriggered(String actionId) {
final cb = _callbacks[actionId];
if (cb != null) {
cb();
} else {
debugPrint('shortcut_triggered: no handler for $actionId');
}
}
/// Read the bindings JSON from LocalConfig.
static List<Map<String, dynamic>> readBindings() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return [];
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
final list = (parsed['bindings'] as List?) ?? [];
return list.cast<Map<String, dynamic>>();
} catch (_) {
return [];
}
}
static bool isEnabled() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return false;
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
return parsed['enabled'] == true;
} catch (_) {
return false;
}
}
}
/// Register the default-bound shortcut actions that aren't already wired by
/// `toolbarControls(...)` (which handles things like Ctrl+Alt+Shift+Del and the
/// screenshot action). Called once per session from the desktop / mobile
/// remote page, after the toolbar registrations have run.
///
/// [tabController] is the desktop window's tab controller; `null` on mobile /
/// web (where tab-switch shortcuts don't apply).
///
/// Each callback below is a no-op when the underlying state required to
/// service the action isn't available (e.g. only one display, only one tab).
void registerSessionShortcutActions(
FFI ffi, {
DesktopTabController? tabController,
}) {
final sessionId = ffi.sessionId;
// Toggle Fullscreen — desktop & web-desktop only. `stateGlobal.setFullscreen`
// handles native window vs. browser fullscreen; on mobile fullscreen is the
// permanent default, so we leave the action unregistered (becomes a logged
// no-op if a mobile user binds it).
if (isDesktop || isWebDesktop) {
ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () {
stateGlobal.setFullscreen(!stateGlobal.fullscreen.value);
});
}
// Switch Display Next / Prev — requires the peer to have at least 2
// displays. No-op when only one display is available or when the user has
// selected the "All displays" pseudo-display.
void switchDisplayBy(int delta) {
final pi = ffi.ffiModel.pi;
final count = pi.displays.length;
if (count <= 1) return;
final current = pi.currentDisplay;
if (current == kAllDisplayValue) return;
final next = ((current + delta) % count + count) % count;
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([next]),
);
if (pi.isSupportMultiUiSession) {
// On multi-ui-session peers no switch-display message is sent back, so
// update the local state directly (mirrors `model.dart` handling).
ffi.ffiModel.switchToNewDisplay(next, sessionId, ffi.id);
}
}
ffi.shortcutModel.register(kShortcutActionSwitchDisplayNext, () {
switchDisplayBy(1);
});
ffi.shortcutModel.register(kShortcutActionSwitchDisplayPrev, () {
switchDisplayBy(-1);
});
// Switch Tab 1..9 — desktop only. The remote-screen tabs live in the
// window-scoped DesktopTabController, not on the FFI itself, so we need
// the controller from the page that owns this session. No-op on mobile /
// web (no controller passed) and when the requested tab index is out of
// range.
if (tabController != null) {
for (var n = 1; n <= 9; n++) {
final idx = n - 1;
ffi.shortcutModel.register(kShortcutActionSwitchTab(n), () {
if (tabController.state.value.tabs.length > idx) {
tabController.jumpTo(idx);
}
});
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:uuid/uuid.dart';
import 'dart:html' as html;
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart' as common;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
@@ -930,6 +931,30 @@ class RustdeskImpl {
]));
}
// Tell the JS-side matcher (flutter/web/js/src/shortcut_matcher.ts) to
// re-read its bindings from LocalStorage. Mirrors the native call which
// refreshes the Rust matcher's in-memory cache.
void mainReloadKeyboardShortcuts({dynamic hint}) {
js.context.callMethod('reloadShortcuts', []);
}
// Mirror of `default_bindings()` in `src/keyboard/shortcuts.rs`. Keep these
// two lists in sync — if you add or change a default binding on the Rust
// side, update the literal below to match.
String mainGetDefaultKeyboardShortcuts({dynamic hint}) {
const prefix = ['primary', 'alt', 'shift'];
final list = <Map<String, dynamic>>[
{'action': 'send_ctrl_alt_del', 'mods': prefix, 'key': 'delete'},
{'action': 'toggle_fullscreen', 'mods': prefix, 'key': 'enter'},
{'action': 'switch_display_next', 'mods': prefix, 'key': 'arrow_right'},
{'action': 'switch_display_prev', 'mods': prefix, 'key': 'arrow_left'},
{'action': 'screenshot', 'mods': prefix, 'key': 'p'},
for (var n = 1; n <= 9; n++)
{'action': 'switch_tab_$n', 'mods': prefix, 'key': 'digit$n'},
];
return jsonEncode(list);
}
String mainGetInputSource({dynamic hint}) {
final inputSource =
js.context.callMethod('getByName', ['option:local', 'input-source']);
@@ -1176,6 +1201,15 @@ class RustdeskImpl {
}
Future<void> mainInit({required String appDir, dynamic hint}) {
// JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/
// shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a
// binding fires; route it to the active session's ShortcutModel.
// Web is single-window so `gFFI` is always the active session.
js.context['onShortcutTriggered'] = (dynamic action) {
if (action is String) {
common.gFFI.shortcutModel.onTriggered(action);
}
};
return Future.value();
}
@@ -1538,7 +1572,10 @@ class RustdeskImpl {
Future<void> mainAccountAuth(
{required String op, required bool rememberMe, dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [
// Safari only allows auth popups while handling the original user gesture.
// Use Future.sync so the JS call runs synchronously (pre-opening the OIDC
// window) while any interop error still surfaces as a Future error.
return Future.sync(() => js.context.callMethod('setByName', [
'account_auth',
jsonEncode({'op': op, 'remember': rememberMe})
]));

View File

@@ -29,6 +29,80 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
extern bool gIsConnectionManager;
// --- Side mouse button support (back/forward) ---
// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart.
// We intercept them via GDK and forward through a dedicated platform channel.
static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons";
static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
if (event->button != 8 && event->button != 9) {
return FALSE;
}
// Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic
// events) - only handle real press and release.
if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) {
return FALSE;
}
FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data);
if (channel == NULL) return FALSE;
g_autoptr(FlValue) args = fl_value_new_map();
fl_value_set_string_take(args, "button",
fl_value_new_string(event->button == 8 ? "back" : "forward"));
fl_value_set_string_take(args, "type",
fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up"));
fl_method_channel_invoke_method(channel, "onSideMouseButton", args,
NULL, NULL, NULL);
return TRUE;
}
static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) {
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
return fl_method_channel_new(
fl_engine_get_binary_messenger(engine),
kSideButtonChannelName,
FL_METHOD_CODEC(codec));
}
static void side_buttons_channel_destroy(gpointer data) {
g_object_unref(data);
}
static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) {
// Guard against double-initialization (would leave dangling signal user_data).
if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return;
gtk_widget_add_events(GTK_WIDGET(window),
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
// Store channel on the window so it stays alive and is freed with the window.
g_object_set_data_full(G_OBJECT(window), "side-buttons-channel",
g_object_ref(channel), side_buttons_channel_destroy);
g_signal_connect(window, "button-press-event",
G_CALLBACK(on_side_button_event), channel);
g_signal_connect(window, "button-release-event",
G_CALLBACK(on_side_button_event), channel);
}
static void on_subwindow_created(FlPluginRegistry* registry) {
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
wayland_shortcuts_inhibit_init_for_subwindow(registry);
#endif
// Set up side button forwarding for sub-windows.
if (registry == NULL || !FL_IS_VIEW(registry)) return;
FlView* view = FL_VIEW(registry);
GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view));
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view));
if (channel == NULL) return;
side_buttons_init_for_window(GTK_WINDOW(toplevel), channel);
g_object_unref(channel); // window now owns a ref via g_object_set_data_full
}
}
GtkWidget *find_gl_area(GtkWidget *widget);
// Implements GApplication::activate.
@@ -96,12 +170,12 @@ static void my_application_activate(GApplication* application) {
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_show(GTK_WIDGET(view));
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
// Register callback for sub-windows created by desktop_multi_window plugin
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
// Register callback for sub-windows created by desktop_multi_window plugin.
// Handles both Wayland shortcuts inhibition (guarded inside) and side button
// forwarding. Safe to call on X11-only builds - the plugin just stores the
// callback pointer regardless of windowing system.
desktop_multi_window_plugin_set_window_created_callback(
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
#endif
(WindowCreatedCallback)on_subwindow_created);
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
@@ -116,6 +190,11 @@ static void my_application_activate(GApplication* application) {
self,
nullptr);
// Forward side mouse button events (back/forward) to Dart on the main window.
FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view));
side_buttons_init_for_window(window, side_channel);
g_object_unref(side_channel);
gtk_widget_grab_focus(GTK_WIDGET(view));
}

View File

@@ -113,8 +113,8 @@ dependencies:
dev_dependencies:
icons_launcher: ^2.0.4
#flutter_test:
#sdk: flutter
flutter_test:
sdk: flutter
build_runner: ^2.4.6
freezed: ^2.4.2
flutter_lints: ^2.0.2

View File

@@ -0,0 +1,125 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/models/input_modifier_utils.dart';
void main() {
group('shouldReleaseStaleMobileShift', () {
test('does not release when cached shift is already false', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: false,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('releases one-shot mobile shift after a text key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('does not release manually toggled shift without tracked key down',
() {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: false,
),
isFalse,
);
});
test('does not release when shift is still physically pressed', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: true,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('does not release on non-mobile platforms', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: false,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('releases on enter key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.enter,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('releases on arrow key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.arrowLeft,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('does not release on modifier events', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.shiftLeft,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('does not release on shiftRight modifier events', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.shiftRight,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
});
}

View File

@@ -12,7 +12,7 @@
//!
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
//! *Need a way to transfer file names with '\' safely*.
//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes.
//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes.
//!
//! # Note
//! - all files on FS should be read only, and mark the owner to be the current user

View File

@@ -8,6 +8,7 @@
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::libc::c_int;
use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay};
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
use std::{borrow::Cow, ffi::CString};
@@ -32,6 +33,51 @@ fn mousebutton(button: MouseButton) -> c_int {
}
}
/// Minimum number of buttons the X11 core pointer must support.
/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons.
const MIN_POINTER_BUTTONS: usize = 9;
/// Check that the X11 core pointer's button map includes at least 9 buttons
/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9).
///
/// RustDesk's uinput "Mouse passthrough" device normally provides enough
/// buttons, but we log a warning if the map is too small so the issue is
/// diagnosable. `XSetPointerMapping` cannot extend the button count (its
/// length must match `XGetPointerMapping`), so we only diagnose here.
fn check_x11_button_map() {
// Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings
// on pure Wayland or headless environments without $DISPLAY.
if std::env::var_os("DISPLAY").is_none() {
return;
}
let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) };
if display.is_null() {
log::warn!("XOpenDisplay failed, cannot check button map");
return;
}
let mut current_map = [0u8; 32];
let nbuttons =
unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) };
unsafe { XCloseDisplay(display) };
if nbuttons < 0 {
log::warn!("XGetPointerMapping failed (returned {nbuttons})");
return;
}
let nbuttons = nbuttons as usize;
if nbuttons >= MIN_POINTER_BUTTONS {
log::info!("X11 pointer has {nbuttons} buttons, side buttons supported");
} else {
log::warn!(
"X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \
back/forward side buttons may not work until a device with more buttons is added"
);
}
}
/// The main struct for handling the event emitting
pub(super) struct EnigoXdo {
xdo: *mut xdo_t,
@@ -52,6 +98,7 @@ impl Default for EnigoXdo {
log::warn!("Failed to create xdo context, xdo functions will be disabled");
} else {
log::info!("xdo context created successfully");
check_x11_button_map();
}
Self {
xdo,

View File

@@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option<Medi
log::error!("Failed to start decoder: {:?}", e);
return None;
};
log::debug!("Init decoder successed!: {:?}", name);
log::debug!("Init decoder succeeded!: {:?}", name);
return Some(MediaCodecDecoder {
decoder: codec,
name: name.to_owned(),

View File

@@ -24,7 +24,7 @@ impl<'a> PixelProvider<'a> {
}
pub trait Recorder {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>>;
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>>;
}
pub trait BoxCloneCapturable {

View File

@@ -346,7 +346,7 @@ impl PipeWireRecorder {
}
impl Recorder for PipeWireRecorder {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>> {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>> {
if let Some(sample) = self
.appsink
.try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms))

View File

@@ -29,4 +29,4 @@ TODO
## X11
## OSX
## macOS

82
res/audits.py Normal file → Executable file
View File

@@ -43,7 +43,7 @@ def get_connection_type_name(conn_type):
"""Convert connection type number to readable name"""
type_map = {
0: "Remote Desktop",
1: "File Transfer",
1: "File Transfer",
2: "Port Transfer",
3: "View Camera",
4: "Terminal"
@@ -55,7 +55,7 @@ def get_console_type_name(console_type):
"""Convert console audit type number to readable name"""
type_map = {
0: "Group Management",
1: "User Management",
1: "User Management",
2: "Device Management",
3: "Address Book Management"
}
@@ -67,7 +67,7 @@ def get_console_operation_name(operation_code):
operation_map = {
0: "User Login",
1: "Add Group",
2: "Add User",
2: "Add User",
3: "Add Device",
4: "Delete Groups",
5: "Disconnect Device",
@@ -95,7 +95,7 @@ def get_console_operation_name(operation_code):
def get_alarm_type_name(alarm_type):
"""Convert alarm type number to readable name"""
type_map = {
0: "Access attempt outside the IP whiltelist",
0: "Access attempt outside the IP whitelist",
1: "Over 30 consecutive access attempts",
2: "Multiple access attempts within one minute",
3: "Over 30 consecutive login attempts",
@@ -109,24 +109,24 @@ def enhance_audit_data(data, audit_type):
"""Enhance audit data with readable formats"""
if not data:
return data
enhanced_data = []
for item in data:
enhanced_item = item.copy()
# Convert timestamps - replace original values
if 'created_at' in enhanced_item:
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
if 'end_time' in enhanced_item:
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
# Add type-specific enhancements - replace original values
if audit_type == 'conn':
if 'conn_type' in enhanced_item:
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
else:
enhanced_item['conn_type'] = "Not Logged In"
elif audit_type == 'console':
if 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
@@ -136,14 +136,14 @@ def enhance_audit_data(data, audit_type):
# Replace iop field with operation and convert to readable name
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
del enhanced_item['iop']
elif audit_type == 'alarm' and 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
del enhanced_item['typ']
enhanced_data.append(enhanced_item)
return enhanced_data
@@ -152,7 +152,7 @@ def check_response(response):
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
try:
response_json = response.json()
if "error" in response_json:
@@ -163,28 +163,28 @@ def check_response(response):
return response.text or "Success"
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
created_at=None, days_ago=None, non_wildcard_fields=None):
"""Common function for viewing audits"""
headers = {"Authorization": f"Bearer {token}"}
# Set default page size and current page
if page_size is None:
page_size = 10
if current is None:
current = 1
params = {
"pageSize": page_size,
"current": current
}
# Add filter parameters if provided
if filters:
for key, value in filters.items():
if value is not None:
params[key] = value
# Handle time filters
if days_ago is not None:
# Calculate datetime from days ago
@@ -205,10 +205,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
# Apply wildcard patterns for string fields (excluding specific fields)
if non_wildcard_fields is None:
non_wildcard_fields = set()
# Always exclude these fields from wildcard treatment
non_wildcard_fields.update(["created_at", "pageSize", "current"])
string_params = {}
for k, v in params.items():
if isinstance(v, str) and k not in non_wildcard_fields:
@@ -221,10 +221,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
response_json = check_response(response)
# Enhance the data with readable formats
data = enhance_audit_data(response_json.get("data", []), endpoint)
return {
"data": data,
"total": response_json.get("total", 0),
@@ -233,7 +233,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
}
def view_conn_audits(url, token, remote=None, conn_type=None,
def view_conn_audits(url, token, remote=None, conn_type=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View connection audits"""
filters = {
@@ -241,7 +241,7 @@ def view_conn_audits(url, token, remote=None, conn_type=None,
"conn_type": conn_type
}
non_wildcard_fields = {"conn_type"}
return view_audits_common(
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -254,7 +254,7 @@ def view_file_audits(url, token, remote=None,
"remote": remote
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -267,7 +267,7 @@ def view_alarm_audits(url, token, device=None,
"device": device
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -280,7 +280,7 @@ def view_console_audits(url, token, operator=None,
"operator": operator
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -295,15 +295,15 @@ def main():
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument("--token", required=True, help="Bearer token for authentication")
# Pagination parameters
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
# Time filtering parameters
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
# Audit filters (simplified)
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
@@ -319,9 +319,9 @@ def main():
if args.command == "view-conn":
# View connection audits
result = view_conn_audits(
args.url,
args.token,
args.remote,
args.url,
args.token,
args.remote,
args.conn_type,
args.page_size,
args.current,
@@ -329,12 +329,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-file":
# View file audits
result = view_file_audits(
args.url,
args.token,
args.url,
args.token,
args.remote,
args.page_size,
args.current,
@@ -342,12 +342,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-alarm":
# View alarm audits
result = view_alarm_audits(
args.url,
args.token,
args.url,
args.token,
args.device,
args.page_size,
args.current,
@@ -355,12 +355,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-console":
# View console audits
result = view_console_audits(
args.url,
args.token,
args.url,
args.token,
args.operator,
args.page_size,
args.current,

View File

@@ -616,10 +616,10 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
if (IsServiceRunningW(svcName)) {
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stoped after 1000 ms.", svcName);
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName);
}
else {
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stoped.", svcName);
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName);
}
if (MyDeleteServiceW(svcName)) {
@@ -645,7 +645,7 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
// It's really strange that we need sleep here.
// But the upgrading may be stucked at "copying new files" because the file is in using.
// But the upgrading may be stuck at "copying new files" because the file is in using.
// Steps to reproduce: Install -> stop service in tray --> start service -> upgrade
// Sleep(300);
@@ -758,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall)
}
// Why RegSetValueExW always return 998?
//
//
result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
if (result != ERROR_SUCCESS) {
WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result);
@@ -874,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc
i = 0;
j = 0;
// svcBinary is a string with double quotes, we need to escape it for shell arguments.
// It is orignal used for `CreateServiceW`.
// It is original used for `CreateServiceW`.
// eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service
while (true) {
if (svcBinary[j] == L'"') {

View File

@@ -25,7 +25,13 @@ impl Session {
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
let mut password = "".to_owned();
if PeerConfig::load(id).password.is_empty() {
password = rpassword::prompt_password("Enter password: ").unwrap();
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(),

View File

@@ -3870,6 +3870,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
&& !text.to_lowercase().contains("resolve")
&& !text.to_lowercase().contains("mismatch")
&& !text.to_lowercase().contains("manually")
&& !text.to_lowercase().contains("restricted")
&& !text.to_lowercase().contains("not allowed")))
}

View File

@@ -586,7 +586,6 @@ impl<T: InvokeUiSession> Remote<T> {
file_num,
include_hidden,
is_remote,
Vec::new(),
od,
));
allow_err!(
@@ -659,7 +658,6 @@ impl<T: InvokeUiSession> Remote<T> {
file_num,
include_hidden,
is_remote,
Vec::new(),
od,
);
job.is_last_job = true;
@@ -845,19 +843,7 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
Data::CancelJob(id) => {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_cancel(FileTransferCancel {
id: id,
..Default::default()
});
msg_out.set_file_action(file_action);
allow_err!(peer.send(&msg_out).await);
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
job.remove_download_file();
}
let _ = fs::remove_job(id, &mut self.read_jobs);
self.remove_jobs.remove(&id);
self.cancel_transfer_job(id, peer).await;
}
Data::RemoveDir((id, path)) => {
let mut msg_out = Message::new();
@@ -1053,6 +1039,22 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_cancel(FileTransferCancel {
id,
..Default::default()
});
msg_out.set_file_action(file_action);
allow_err!(peer.send(&msg_out).await);
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
job.remove_download_file();
}
let _ = fs::remove_job(id, &mut self.read_jobs);
self.remove_jobs.remove(&id);
}
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
if !self.is_connected {
return false;
@@ -1446,6 +1448,23 @@ impl<T: InvokeUiSession> Remote<T> {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
#[cfg(target_os = "ios")]
{
if let Some(cb) = _mcb
.clipboards
.iter()
.find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text))
{
let content = if cb.compress {
hbb_common::compress::decompress(&cb.content)
} else {
cb.content.to_vec()
};
if let Ok(content) = String::from_utf8(content) {
self.handler.clipboard(content);
}
}
}
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
}
@@ -1470,14 +1489,43 @@ impl<T: InvokeUiSession> Remote<T> {
fs::transform_windows_path(&mut entries);
}
}
self.handler
.update_folder_files(fd.id, &entries, fd.path, false, false);
// We cannot call cancel_transfer_job/handle_job_status while holding
// a mutable borrow from fs::get_job(&mut self.write_jobs), so defer
// the error handling until after the borrow scope ends.
let mut set_files_err = None;
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
log::info!("job set_files: {:?}", entries);
job.set_files(entries);
job.set_finished_size_on_resume();
if let Err(err) = job.set_files(entries) {
set_files_err = Some(err.to_string());
} else {
job.set_finished_size_on_resume();
self.handler.update_folder_files(
fd.id,
job.files(),
fd.path,
false,
false,
);
}
} else if let Some(job) = self.remove_jobs.get_mut(&fd.id) {
// Intentionally keep raw entries here:
// - remote remove flow executes deletions on peer side;
// - local remove flow is populated from local get_recursive_files().
job.files = entries;
self.handler
.update_folder_files(fd.id, &job.files, fd.path, false, false);
} else {
self.handler
.update_folder_files(fd.id, &entries, fd.path, false, false);
}
if let Some(err) = set_files_err {
log::warn!(
"Rejected unsafe file list from remote peer for job {}: {}",
fd.id,
err
);
self.cancel_transfer_job(fd.id, peer).await;
self.handle_job_status(fd.id, -1, Some(err));
}
}
Some(file_response::Union::Digest(digest)) => {

View File

@@ -39,7 +39,7 @@ use hbb_common::{
use crate::{
hbbs_http::{create_http_client_async, get_url_for_tls},
ui_interface::{get_option, is_installed, set_option},
ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
};
#[derive(Debug, Eq, PartialEq)]
@@ -1086,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
#[inline]
pub fn is_public(url: &str) -> bool {
let url = url.to_ascii_lowercase();
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
}
@@ -1123,22 +1124,286 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
format!("{}/api/audit/{}", url, typ)
}
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
/// Check if we should use raw TCP proxy for API calls.
/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
/// and the target URL belongs to the configured non-public API host.
#[inline]
fn should_use_raw_tcp_for_api(url: &str) -> bool {
get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
&& !use_ws()
&& is_tcp_proxy_api_target(url)
}
/// Check if we can attempt raw TCP proxy fallback for this target URL.
#[inline]
fn can_fallback_to_raw_tcp(url: &str) -> bool {
!use_ws() && is_tcp_proxy_api_target(url)
}
#[inline]
fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
if api_url.is_empty() || is_public(api_url) {
return false;
}
let target_host = url::Url::parse(url)
.ok()
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
let api_host = url::Url::parse(api_url)
.ok()
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
}
#[inline]
fn is_tcp_proxy_api_target(url: &str) -> bool {
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
}
fn tcp_proxy_log_target(url: &str) -> String {
url::Url::parse(url)
.ok()
.map(|parsed| {
let mut redacted = format!("{}://", parsed.scheme());
let Some(host) = parsed.host() else {
return "<invalid-url>".to_owned();
};
redacted.push_str(&host.to_string());
if let Some(port) = parsed.port() {
redacted.push(':');
redacted.push_str(&port.to_string());
}
redacted.push_str(parsed.path());
redacted
})
.unwrap_or_else(|| "<invalid-url>".to_owned())
}
#[inline]
fn get_tcp_proxy_addr() -> String {
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
}
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
/// receives `HttpProxyResponse`.
///
/// The entire operation (connect + handshake + send + receive) is wrapped in
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
/// any stage cannot block the caller indefinitely.
async fn tcp_proxy_request(
method: &str,
url: &str,
body: &[u8],
headers: Vec<HeaderEntry>,
) -> ResultType<HttpProxyResponse> {
let tcp_addr = get_tcp_proxy_addr();
if tcp_addr.is_empty() {
bail!("No rendezvous server configured for TCP proxy");
}
let parsed = url::Url::parse(url)?;
let path = if let Some(query) = parsed.query() {
format!("{}?{}", parsed.path(), query)
} else {
parsed.path().to_string()
};
log::debug!(
"Sending {} {} via TCP proxy to {}",
method,
parsed.path(),
tcp_addr
);
let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
timeout(overall_timeout, async {
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await;
secure_tcp_silent(&mut conn, &key).await?;
let mut req = HttpProxyRequest::new();
req.method = method.to_uppercase();
req.path = path;
req.headers = headers.into();
req.body = Bytes::from(body.to_vec());
let mut msg_out = RendezvousMessage::new();
msg_out.set_http_proxy_request(req);
conn.send(&msg_out).await?;
match conn.next().await {
Some(Ok(bytes)) => {
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
match msg_in.union {
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
_ => bail!("Unexpected response from TCP proxy"),
}
}
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
None => bail!("TCP proxy connection closed without response"),
}
})
.await?
}
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
let mut entries = Vec::new();
let mut has_content_type = false;
if !header.is_empty() {
let tmp: Vec<&str> = header.splitn(2, ": ").collect();
if tmp.len() == 2 {
if tmp[0].eq_ignore_ascii_case("Content-Type") {
has_content_type = true;
}
entries.push(HeaderEntry {
name: tmp[0].into(),
value: tmp[1].into(),
..Default::default()
});
}
}
if !has_content_type {
entries.insert(
0,
HeaderEntry {
name: "Content-Type".into(),
value: "application/json".into(),
..Default::default()
},
);
}
entries
}
/// POST request via TCP proxy.
async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
let headers = parse_simple_header(header);
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
if !resp.error.is_empty() {
bail!("TCP proxy error: {}", resp.error);
}
Ok(String::from_utf8_lossy(&resp.body).to_string())
}
fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
if !resp.error.is_empty() {
bail!("TCP proxy error: {}", resp.error);
}
let mut response_headers = Map::new();
for entry in resp.headers.iter() {
response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
}
let mut result = Map::new();
result.insert("status_code".to_string(), json!(resp.status));
result.insert("headers".to_string(), Value::Object(response_headers));
result.insert(
"body".to_string(),
json!(String::from_utf8_lossy(&resp.body)),
);
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
}
fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
let v: Value = serde_json::from_str(header)?;
if let Value::Object(obj) = v {
Ok(obj
.iter()
.map(|(key, value)| HeaderEntry {
name: key.clone(),
value: value.as_str().unwrap_or_default().into(),
..Default::default()
})
.collect())
} else {
Err(anyhow!("HTTP header information parsing failed!"))
}
}
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_url = get_url_for_tls(url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = post_request_(
&url,
url,
tls_url,
body.clone(),
body.to_owned(),
header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await?;
Ok(response.text().await?)
let status = response.status().as_u16();
let text = response.text().await?;
Ok((status, text))
}
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
/// if the URL is eligible. 4xx responses are returned as-is.
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
url: &str,
method: &str,
http_fn: HttpFut,
tcp_fn: TcpFut,
) -> ResultType<String>
where
HttpFut: Future<Output = ResultType<(u16, String)>>,
TcpFut: Future<Output = ResultType<String>>,
{
if should_use_raw_tcp_for_api(url) {
return tcp_fn.await;
}
let http_result = http_fn.await;
let should_fallback = match &http_result {
Err(_) => true,
Ok((status, _)) => *status >= 500,
};
if should_fallback && can_fallback_to_raw_tcp(url) {
log::warn!(
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
method,
tcp_proxy_log_target(url),
http_result
.as_ref()
.map(|(s, _)| *s)
.map_err(|e| e.to_string()),
);
match tcp_fn.await {
Ok(resp) => return Ok(resp),
Err(tcp_err) => {
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
}
}
}
http_result.map(|(_status, text)| text)
}
/// POST request with raw TCP proxy support.
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
/// falls back to TCP proxy if WS is off.
/// - 4xx responses are returned as-is (server is reachable, business logic error).
/// - If fallback also fails, returns the original HTTP result (text or error).
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
with_tcp_proxy_fallback(
&url,
"POST",
post_request_http(&url, &body, header),
post_request_via_tcp_proxy(&url, &body, header),
)
.await
}
#[async_recursion]
@@ -1246,21 +1511,16 @@ async fn get_http_response_async(
tls_type.unwrap_or(TlsType::Rustls),
danger_accept_invalid_cert.unwrap_or(false),
);
let mut http_client = match method {
let normalized_method = method.to_ascii_lowercase();
let mut http_client = match normalized_method.as_str() {
"get" => http_client.get(url),
"post" => http_client.post(url),
"put" => http_client.put(url),
"delete" => http_client.delete(url),
_ => return Err(anyhow!("The HTTP request method is not supported!")),
};
let v = serde_json::from_str(header)?;
if let Value::Object(obj) = v {
for (key, value) in obj.iter() {
http_client = http_client.header(key, value.as_str().unwrap_or_default());
}
} else {
return Err(anyhow!("HTTP header information parsing failed!"));
for entry in parse_json_header_entries(header)? {
http_client = http_client.header(entry.name, entry.value);
}
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
@@ -1340,6 +1600,51 @@ async fn get_http_response_async(
}
}
/// Returns (status_code, json_string) so the caller can inspect the status
/// without re-parsing the serialized JSON.
async fn http_request_http(
url: &str,
method: &str,
body: Option<String>,
header: &str,
) -> ResultType<(u16, String)> {
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = get_http_response_async(
url,
tls_url,
method,
body,
header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await?;
// Serialize response headers
let mut response_headers = Map::new();
for (key, value) in response.headers() {
response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
}
let status_code = response.status().as_u16();
let response_body = response.text().await?;
// Construct the JSON object
let mut result = Map::new();
result.insert("status_code".to_string(), json!(status_code));
result.insert("headers".to_string(), Value::Object(response_headers));
result.insert("body".to_string(), json!(response_body));
// Convert map to JSON string
let json_str = serde_json::to_string(&result)
.map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
Ok((status_code, json_str))
}
/// HTTP request with raw TCP proxy support.
#[tokio::main(flavor = "current_thread")]
pub async fn http_request_sync(
url: String,
@@ -1347,44 +1652,28 @@ pub async fn http_request_sync(
body: Option<String>,
header: String,
) -> ResultType<String> {
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = get_http_response_async(
with_tcp_proxy_fallback(
&url,
tls_url,
&method,
body.clone(),
&header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
http_request_http(&url, &method, body.clone(), &header),
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
)
.await?;
// Serialize response headers
let mut response_headers = serde_json::map::Map::new();
for (key, value) in response.headers() {
response_headers.insert(
key.to_string(),
serde_json::json!(value.to_str().unwrap_or("")),
);
}
.await
}
let status_code = response.status().as_u16();
let response_body = response.text().await?;
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
/// Returns a JSON string with status_code, headers, body (same format as http_request_sync).
async fn http_request_via_tcp_proxy(
url: &str,
method: &str,
body: Option<&str>,
header: &str,
) -> ResultType<String> {
let headers = parse_json_header_entries(header)?;
let body_bytes = body.unwrap_or("").as_bytes();
// Construct the JSON object
let mut result = serde_json::map::Map::new();
result.insert("status_code".to_string(), serde_json::json!(status_code));
result.insert(
"headers".to_string(),
serde_json::Value::Object(response_headers),
);
result.insert("body".to_string(), serde_json::json!(response_body));
// Convert map to JSON string
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
http_proxy_response_to_json(resp)
}
#[inline]
@@ -1647,7 +1936,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
false
}
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
// Skip additional encryption when using WebSocket connections (wss://)
// as WebSocket Secure (wss://) already provides transport layer encryption.
// This doesn't affect the end-to-end encryption between clients,
@@ -1680,7 +1969,9 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
});
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
conn.set_key(key);
log::info!("Connection secured");
if log_on_success {
log::info!("Connection secured");
}
}
_ => {}
}
@@ -1691,6 +1982,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
Ok(())
}
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, true).await
}
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, false).await
}
#[inline]
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
if pk.len() == 32 {
@@ -2468,11 +2767,13 @@ mod tests {
assert!(is_public("https://rustdesk.com/"));
assert!(is_public("https://www.rustdesk.com/"));
assert!(is_public("https://api.rustdesk.com/v1"));
assert!(is_public("https://API.RUSTDESK.COM/v1"));
assert!(is_public("https://rustdesk.com/path"));
// Test URLs ending with "rustdesk.com"
assert!(is_public("rustdesk.com"));
assert!(is_public("https://rustdesk.com"));
assert!(is_public("https://RustDesk.com"));
assert!(is_public("http://www.rustdesk.com"));
assert!(is_public("https://api.rustdesk.com"));
@@ -2485,6 +2786,193 @@ mod tests {
assert!(!is_public("rustdesk.comhello.com"));
}
#[test]
fn test_should_use_tcp_proxy_for_api_url() {
assert!(should_use_tcp_proxy_for_api_url(
"https://admin.example.com/api/login",
"https://admin.example.com"
));
assert!(should_use_tcp_proxy_for_api_url(
"https://admin.example.com:21114/api/login",
"https://admin.example.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://api.telegram.org/bot123/sendMessage",
"https://admin.example.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://admin.rustdesk.com/api/login",
"https://admin.rustdesk.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://admin.example.com/api/login",
"not a url"
));
assert!(!should_use_tcp_proxy_for_api_url(
"not a url",
"https://admin.example.com"
));
}
#[test]
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
struct RestoreCustomRendezvousServer(String);
impl Drop for RestoreCustomRendezvousServer {
fn drop(&mut self) {
Config::set_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
self.0.clone(),
);
}
}
let _restore = RestoreCustomRendezvousServer(Config::get_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
));
Config::set_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
"1:2".to_string(),
);
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
}
#[tokio::test]
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
.await
.unwrap_err()
.to_string();
assert!(err.contains("HTTP header information parsing failed!"));
}
#[test]
fn test_parse_json_header_entries_preserves_single_content_type() {
let headers = parse_json_header_entries(
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
)
.unwrap();
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("text/plain")
);
}
#[test]
fn test_parse_json_header_entries_does_not_add_default_content_type() {
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
assert!(!headers
.iter()
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
}
#[test]
fn test_parse_simple_header_respects_custom_content_type() {
let headers = parse_simple_header("Content-Type: text/plain");
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("text/plain")
);
}
#[test]
fn test_parse_simple_header_preserves_non_content_type_header() {
let headers = parse_simple_header("Authorization: Bearer token");
assert!(headers.iter().any(|entry| {
entry.name.eq_ignore_ascii_case("Authorization")
&& entry.value.as_str() == "Bearer token"
}));
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("application/json")
);
}
#[test]
fn test_tcp_proxy_log_target_redacts_query_only() {
assert_eq!(
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
"https://example.com/api/heartbeat"
);
}
#[test]
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
assert_eq!(
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
"https://[2001:db8::1]:21114/api/heartbeat"
);
}
#[test]
fn test_http_proxy_response_to_json() {
let mut resp = HttpProxyResponse {
status: 200,
body: br#"{"ok":true}"#.to_vec().into(),
..Default::default()
};
resp.headers.push(HeaderEntry {
name: "Content-Type".into(),
value: "application/json".into(),
..Default::default()
});
let json = http_proxy_response_to_json(resp).unwrap();
let value: Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["status_code"], 200);
assert_eq!(value["headers"]["content-type"], "application/json");
assert_eq!(value["body"], r#"{"ok":true}"#);
let err = http_proxy_response_to_json(HttpProxyResponse {
error: "dial failed".into(),
..Default::default()
})
.unwrap_err()
.to_string();
assert!(err.contains("TCP proxy error: dial failed"));
}
#[test]
fn test_mouse_event_constants_and_mask_layout() {
use super::input::*;

View File

@@ -575,6 +575,7 @@ pub fn session_handle_flutter_key_event(
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event(
session_id,
&keyboard_mode,
&character,
usb_hid,
@@ -595,6 +596,7 @@ pub fn session_handle_flutter_raw_key_event(
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_raw_key_event(
session_id,
&keyboard_mode,
&name,
platform_code,
@@ -605,21 +607,30 @@ pub fn session_handle_flutter_raw_key_event(
}
}
// SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called.
//
// If the cursor jumps between remote page of two connections, leave view and enter view will be called.
// session_enter_or_leave() will be called then.
// As rust is multi-thread, it is possible that enter() is called before leave().
// This will cause the keyboard input to take no effect.
// As Rust is multi-threaded, enter() can be called before leave().
// The Rust-side grab ownership state filters stale transitions.
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
let keyboard_mode = session.get_keyboard_mode();
// Use the full per-window UUID (not lc.session_id which is per-connection)
// so that two windows viewing the same peer get distinct grab owners.
let window_id = _session_id.as_u128();
if _enter {
set_cur_session_id_(_session_id, &keyboard_mode);
session.enter(keyboard_mode);
crate::keyboard::client::change_grab_status(
crate::common::GrabState::Run,
&keyboard_mode,
window_id,
);
} else {
session.leave(keyboard_mode);
crate::keyboard::client::change_grab_status(
crate::common::GrabState::Wait,
&keyboard_mode,
window_id,
);
}
}
SyncReturn(())
@@ -1719,6 +1730,7 @@ pub fn cm_get_clients_length() -> usize {
pub fn main_init(app_dir: String, custom_client_config: String) {
initialize(&app_dir, &custom_client_config);
crate::keyboard::shortcuts::reload_from_config();
}
pub fn main_device_id(id: String) {
@@ -2238,6 +2250,17 @@ pub fn main_init_input_source() -> SyncReturn<()> {
SyncReturn(())
}
pub fn main_reload_keyboard_shortcuts() -> SyncReturn<()> {
crate::keyboard::shortcuts::reload_from_config();
SyncReturn(())
}
pub fn main_get_default_keyboard_shortcuts() -> SyncReturn<String> {
let bindings = crate::keyboard::shortcuts::default_bindings();
let json = serde_json::to_string(&bindings).unwrap_or_default();
SyncReturn(json)
}
pub fn main_is_installed_lower_version() -> SyncReturn<bool> {
SyncReturn(is_installed_lower_version())
}
@@ -2884,7 +2907,7 @@ pub fn main_set_common(_key: String, _value: String) {
} else if _key == "update-me" {
if let Some(new_version_file) = get_download_file_from_url(&_value) {
log::debug!(
"New version file is downloaed, update begin, {:?}",
"New version file is downloaded, update begin, {:?}",
new_version_file.to_str()
);
if let Some(f) = new_version_file.to_str() {

View File

@@ -1,4 +1,4 @@
use reqwest::blocking::Response;
use hbb_common::ResultType;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
@@ -21,11 +21,9 @@ pub enum HbbHttpResponse<T> {
Data(T),
}
impl<T: DeserializeOwned> TryFrom<Response> for HbbHttpResponse<T> {
type Error = reqwest::Error;
fn try_from(resp: Response) -> Result<Self, <Self as TryFrom<Response>>::Error> {
let map = resp.json::<Map<String, Value>>()?;
impl<T: DeserializeOwned> HbbHttpResponse<T> {
pub fn parse(body: &str) -> ResultType<Self> {
let map = serde_json::from_str::<Map<String, Value>>(body)?;
if let Some(error) = map.get("error") {
if let Some(err) = error.as_str() {
Ok(Self::Error(err.to_owned()))

View File

@@ -1,7 +1,6 @@
use super::HbbHttpResponse;
use crate::hbbs_http::create_http_client_with_url;
use hbb_common::{config::LocalConfig, log, ResultType};
use reqwest::blocking::Client;
use serde_derive::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::{
@@ -109,7 +108,7 @@ pub struct AuthBody {
}
pub struct OidcSession {
client: Option<Client>,
warmed_api_server: Option<String>,
state_msg: &'static str,
failed_msg: String,
code_url: Option<OidcAuthUrl>,
@@ -136,7 +135,7 @@ impl Default for UserStatus {
impl OidcSession {
fn new() -> Self {
Self {
client: None,
warmed_api_server: None,
state_msg: REQUESTING_ACCOUNT_AUTH,
failed_msg: "".to_owned(),
code_url: None,
@@ -149,12 +148,13 @@ impl OidcSession {
fn ensure_client(api_server: &str) {
let mut write_guard = OIDC_SESSION.write().unwrap();
if write_guard.client.is_none() {
// This URL is used to detect the appropriate TLS implementation for the server.
let login_option_url = format!("{}/api/login-options", &api_server);
let client = create_http_client_with_url(&login_option_url);
write_guard.client = Some(client);
if write_guard.warmed_api_server.as_deref() == Some(api_server) {
return;
}
// This URL is used to detect the appropriate TLS implementation for the server.
let login_option_url = format!("{}/api/login-options", api_server);
let _ = create_http_client_with_url(&login_option_url);
write_guard.warmed_api_server = Some(api_server.to_owned());
}
fn auth(
@@ -164,26 +164,15 @@ impl OidcSession {
uuid: &str,
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
Self::ensure_client(api_server);
let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client {
client
.post(format!("{}/api/oidc/auth", api_server))
.json(&serde_json::json!({
"op": op,
"id": id,
"uuid": uuid,
"deviceInfo": crate::ui_interface::get_login_device_info(),
}))
.send()?
} else {
hbb_common::bail!("http client not initialized");
};
let status = resp.status();
match resp.try_into() {
Ok(v) => Ok(v),
Err(err) => {
hbb_common::bail!("Http status: {}, err: {}", status, err);
}
}
let body = serde_json::json!({
"op": op,
"id": id,
"uuid": uuid,
"deviceInfo": crate::ui_interface::get_login_device_info(),
})
.to_string();
let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?;
HbbHttpResponse::parse(&resp)
}
fn query(
@@ -197,11 +186,19 @@ impl OidcSession {
&[("code", code), ("id", id), ("uuid", uuid)],
)?;
Self::ensure_client(api_server);
if let Some(client) = &OIDC_SESSION.read().unwrap().client {
Ok(client.get(url).send()?.try_into()?)
} else {
hbb_common::bail!("http client not initialized")
#[derive(Deserialize)]
struct HttpResponseBody {
body: String,
}
let resp = crate::http_request_sync(
url.to_string(),
"GET".to_owned(),
None,
"{}".to_owned(),
)?;
let resp = serde_json::from_str::<HttpResponseBody>(&resp)?;
HbbHttpResponse::parse(&resp.body)
}
fn reset(&mut self) {

View File

@@ -10,6 +10,7 @@ use crate::{client::get_key_state, common::GrabState};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::log;
use hbb_common::message_proto::*;
use hbb_common::SessionID;
#[cfg(any(target_os = "windows", target_os = "macos"))]
use rdev::KeyCode;
use rdev::{Event, EventType, Key};
@@ -79,11 +80,72 @@ lazy_static::lazy_static! {
};
}
pub mod shortcuts;
pub mod client {
use super::*;
/// Tracks grab ownership and serializes transitions across threads.
///
/// Multiple Flutter isolates (one per session window) call
/// `change_grab_status(Run/Wait)` concurrently. Without serialization a
/// stale `Wait` from session A can clobber session B's freshly acquired
/// grab on any desktop OS.
///
/// Windows and macOS are less susceptible in practice because the Flutter
/// side triggers `enterView` only after a mouse click inside the window,
/// but we cannot rely on that. On Linux/X11, `XGrabKeyboard` can also
/// cause a focus-change feedback loop (~10 Hz), so `last_grab` debounces
/// spurious `Wait` events that arrive shortly after a `Run`.
#[derive(Default)]
struct GrabOwnerState {
owner: Option<u128>,
last_grab: Option<std::time::Instant>,
/// True while a deferred-release thread is in flight. Prevents
/// spawning redundant threads during the X11 feedback loop.
deferred_pending: bool,
}
/// How long after a grab acquisition we suppress Wait from the same session.
/// Must exceed one full X11 feedback cycle (~100 ms: 50 ms enable + 50 ms disable).
#[cfg(target_os = "linux")]
const GRAB_DEBOUNCE_MS: u128 = 300;
lazy_static::lazy_static! {
static ref IS_GRAB_STARTED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
static ref GRAB_STATE: Arc<Mutex<GrabOwnerState>> = Arc::new(Mutex::new(GrabOwnerState::default()));
}
#[cfg(target_os = "linux")]
lazy_static::lazy_static! {
static ref GRAB_OP_LOCK: Mutex<()> = Mutex::new(());
}
#[cfg(target_os = "linux")]
fn apply_run_grab_if_owner(session_id: u128, disable_first: bool) {
let _lock = GRAB_OP_LOCK.lock().unwrap();
let gs = GRAB_STATE.lock().unwrap();
if gs.owner != Some(session_id) {
return;
}
drop(gs);
if disable_first {
log::debug!("[grab] handoff: disable_grab before re-grab");
rdev::disable_grab();
}
rdev::enable_grab();
}
#[cfg(target_os = "linux")]
fn disable_grab_if_released() {
let _lock = GRAB_OP_LOCK.lock().unwrap();
let should_disable = {
let gs = GRAB_STATE.lock().unwrap();
gs.owner.is_none() && gs.last_grab.is_none()
};
if should_disable {
rdev::disable_grab();
}
}
pub fn start_grab_loop() {
@@ -96,39 +158,197 @@ pub mod client {
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn change_grab_status(state: GrabState, keyboard_mode: &str) {
pub fn change_grab_status(state: GrabState, keyboard_mode: &str, session_id: u128) {
#[cfg(feature = "flutter")]
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
return;
}
// Serialize transitions so a stale `Wait` from a previous owner cannot
// clobber a fresh `Run` from a different session window.
let mut release_after_unlock = None;
#[cfg(target_os = "linux")]
let mut run_grab_after_unlock = None;
#[cfg(target_os = "linux")]
let mut disable_after_unlock = false;
let mut gs = GRAB_STATE.lock().unwrap();
match state {
GrabState::Ready => {}
GrabState::Run => {
#[cfg(windows)]
update_grab_get_key_name(keyboard_mode);
// Idempotent: if this session already owns the grab, just
// refresh the debounce timer (proves the session is still
// actively focused) and skip the actual grab call.
if gs.owner == Some(session_id) {
gs.last_grab = Some(std::time::Instant::now());
// Reset so the next Wait can spawn a fresh deferred-release
// timer with an up-to-date snapshot of last_grab.
gs.deferred_pending = false;
log::debug!(
"[grab] Run(0x{:x}): already owner, refresh debounce",
session_id
);
return;
}
log::debug!(
"[grab] Run(0x{:x}): prev_owner={}, mode={}",
session_id,
gs.owner
.map_or("none".to_string(), |id| format!("0x{:x}", id)),
keyboard_mode,
);
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
KEYBOARD_HOOKED.swap(true, Ordering::SeqCst);
KEYBOARD_HOOKED.store(true, Ordering::SeqCst);
#[cfg(target_os = "linux")]
rdev::enable_grab();
let had_owner = gs.owner.is_some();
gs.owner = Some(session_id);
gs.last_grab = Some(std::time::Instant::now());
// Invalidate any in-flight deferred release from the previous
// owner so it cannot suppress a fresh timer for the new owner.
gs.deferred_pending = false;
#[cfg(target_os = "linux")]
{
run_grab_after_unlock = Some(had_owner);
}
}
GrabState::Wait => {
// Drop stale `Wait` events that do not correspond to the
// current grab owner. This prevents a late PointerExit from
// session A from releasing session B's freshly acquired grab.
if gs.owner != Some(session_id) {
log::debug!(
"[grab] Wait(0x{:x}): ignored, owner={}",
session_id,
gs.owner
.map_or("none".to_string(), |id| format!("0x{:x}", id)),
);
return;
}
// Debounce: on Linux/X11, XGrabKeyboard causes a focus-change
// feedback loop (grab -> PointerExit -> ungrab -> PointerEnter ->
// grab -> ...). Suppress Wait if the grab was acquired recently
// by this same session -- it is X11 feedback, not a real leave.
// A deferred release is scheduled so that a genuine leave within
// the debounce window is not permanently lost.
#[cfg(target_os = "linux")]
if let Some(t) = gs.last_grab {
let elapsed = t.elapsed().as_millis();
if elapsed < GRAB_DEBOUNCE_MS {
if !gs.deferred_pending {
log::debug!(
"[grab] Wait(0x{:x}): debounced ({}ms < {}ms), scheduling deferred release",
session_id, elapsed, GRAB_DEBOUNCE_MS,
);
gs.deferred_pending = true;
let remaining = (GRAB_DEBOUNCE_MS - elapsed) as u64 + 50;
let snapshot = gs.last_grab;
let mode = keyboard_mode.to_string();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(remaining));
let release_keys = {
let mut gs = GRAB_STATE.lock().unwrap();
// Release only if no new Run has refreshed the grab since.
if gs.owner == Some(session_id) && gs.last_grab == snapshot {
let to_release = take_remote_keys();
gs.deferred_pending = false;
log::debug!(
"[grab] Wait(0x{:x}): deferred release",
session_id
);
KEYBOARD_HOOKED.store(false, Ordering::SeqCst);
gs.owner = None;
gs.last_grab = None;
Some(to_release)
} else {
log::debug!(
"[grab] Wait(0x{:x}): deferred release cancelled (grab refreshed)",
session_id,
);
None
}
};
if let Some(to_release) = release_keys {
disable_grab_if_released();
release_remote_keys_for_events(&mode, to_release);
}
});
} else {
log::debug!(
"[grab] Wait(0x{:x}): debounced, deferred release already pending",
session_id,
);
}
return;
}
}
log::debug!("[grab] Wait(0x{:x}): releasing grab", session_id);
#[cfg(windows)]
rdev::set_get_key_unicode(false);
release_remote_keys(keyboard_mode);
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
KEYBOARD_HOOKED.swap(false, Ordering::SeqCst);
KEYBOARD_HOOKED.store(false, Ordering::SeqCst);
gs.owner = None;
gs.last_grab = None;
gs.deferred_pending = false;
release_after_unlock = Some(take_remote_keys());
#[cfg(target_os = "linux")]
rdev::disable_grab();
{
disable_after_unlock = true;
}
}
GrabState::Exit => {}
}
drop(gs);
#[cfg(target_os = "linux")]
{
if disable_after_unlock {
disable_grab_if_released();
}
if let Some(disable_first) = run_grab_after_unlock {
apply_run_grab_if_owner(session_id, disable_first);
}
}
if let Some(to_release) = release_after_unlock {
release_remote_keys_for_events(keyboard_mode, to_release);
}
}
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
// Shortcut intercept — must come before any wire encoding.
// Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// for KeyRelease and other non-press events), so flushed releases from
// release_remote_keys pass straight through to the encode/forward path.
if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
#[cfg(feature = "flutter")]
{
// The rdev grab loop is genuinely process-wide: it does not know which
// Flutter SessionID the keystroke was meant for, so we route to the
// globally-current session via flutter::get_cur_session_id() (maintained
// by session_enter_or_leave). This is the only behavior available on the
// rdev path; the Flutter path threads the explicit per-call SessionID
// through process_event_with_session instead.
let session_id = crate::flutter::get_cur_session_id();
crate::flutter::push_session_event(
&session_id,
"shortcut_triggered",
vec![("action", &action_id)],
);
}
#[cfg(not(feature = "flutter"))]
{
let _ = action_id;
}
return;
}
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
if is_long_press(&event) {
return;
@@ -144,7 +364,33 @@ pub mod client {
event: &Event,
lock_modes: Option<i32>,
session: &Session<T>,
session_id: SessionID,
) {
// Shortcut intercept — must come before any wire encoding.
// Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// for KeyRelease and other non-press events), so flushed releases from
// release_remote_keys pass straight through to the encode/forward path.
if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
#[cfg(feature = "flutter")]
{
// The Flutter path threads the explicit SessionID from the FFI entry
// (session_handle_flutter_*key_event) through this call, so the dispatch
// targets the exact tab the keystroke originated from — no dependency on
// the global focus tracker and no multi-window race.
crate::flutter::push_session_event(
&session_id,
"shortcut_triggered",
vec![("action", &action_id)],
);
}
#[cfg(not(feature = "flutter"))]
{
let _ = action_id;
let _ = session_id;
}
return;
}
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
if is_long_press(&event) {
return;
@@ -341,7 +587,6 @@ fn notify_exit_relative_mouse_mode() {
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
}
/// Handle relative mouse mode shortcuts in the rdev grab loop.
/// Returns true if the event should be blocked from being sent to the peer.
#[cfg(feature = "flutter")]
@@ -540,10 +785,12 @@ pub fn is_long_press(event: &Event) -> bool {
return false;
}
pub fn release_remote_keys(keyboard_mode: &str) {
// todo!: client quit suddenly, how to release keys?
let to_release = TO_RELEASE.lock().unwrap().clone();
TO_RELEASE.lock().unwrap().clear();
fn take_remote_keys() -> HashMap<Key, Event> {
let mut to_release = TO_RELEASE.lock().unwrap();
std::mem::take(&mut *to_release)
}
fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap<Key, Event>) {
for (key, mut event) in to_release.into_iter() {
event.event_type = EventType::KeyRelease(key);
client::process_event(keyboard_mode, &event, None);
@@ -558,6 +805,12 @@ pub fn release_remote_keys(keyboard_mode: &str) {
}
}
#[allow(dead_code)]
pub fn release_remote_keys(keyboard_mode: &str) {
// todo!: client quit suddenly, how to release keys?
release_remote_keys_for_events(keyboard_mode, take_remote_keys());
}
pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode {
match keyboard_mode {
"map" => KeyboardMode::Map,
@@ -748,7 +1001,6 @@ pub fn event_to_key_events(
) -> Vec<KeyEvent> {
peer.retain(|c| !c.is_whitespace());
let mut key_event = KeyEvent::new();
update_modifiers_state(event);
match event.event_type {
@@ -761,6 +1013,7 @@ pub fn event_to_key_events(
_ => {}
}
let mut key_event = KeyEvent::new();
key_event.mode = keyboard_mode.into();
let mut key_events = match keyboard_mode {

370
src/keyboard/shortcuts.rs Normal file
View File

@@ -0,0 +1,370 @@
//! Keyboard shortcuts for triggering session actions locally.
use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts";
lazy_static::lazy_static! {
static ref CACHE: RwLock<Arc<Bindings>> = RwLock::new(Arc::new(Bindings::default()));
}
/// Registry of all valid action ids that may appear in `Binding.action`.
/// Source-of-truth lives on the Flutter side (`flutter/lib/consts.dart`,
/// `kShortcutAction*`); these mirror that vocabulary so Rust code can reach
/// for them without re-stringifying.
#[allow(dead_code)]
pub mod action_id {
pub const SEND_CTRL_ALT_DEL: &str = "send_ctrl_alt_del";
pub const TOGGLE_FULLSCREEN: &str = "toggle_fullscreen";
pub const SWITCH_DISPLAY_NEXT: &str = "switch_display_next";
pub const SWITCH_DISPLAY_PREV: &str = "switch_display_prev";
pub const SCREENSHOT: &str = "screenshot";
pub const INSERT_LOCK: &str = "insert_lock";
pub const REFRESH: &str = "refresh";
pub const TOGGLE_AUDIO: &str = "toggle_audio";
pub const TOGGLE_BLOCK_INPUT: &str = "toggle_block_input";
pub const TOGGLE_RECORDING: &str = "toggle_recording";
pub const TOGGLE_PRIVACY_MODE: &str = "toggle_privacy_mode";
pub const VIEW_MODE_1_TO_1: &str = "view_mode_1_to_1";
pub const VIEW_MODE_SHRINK: &str = "view_mode_shrink";
pub const VIEW_MODE_STRETCH: &str = "view_mode_stretch";
pub const SWITCH_SIDES: &str = "switch_sides";
// switch_tab_1 .. switch_tab_9 are generated below.
}
pub fn switch_tab_action_id(n: u8) -> Option<&'static str> {
match n {
1 => Some("switch_tab_1"),
2 => Some("switch_tab_2"),
3 => Some("switch_tab_3"),
4 => Some("switch_tab_4"),
5 => Some("switch_tab_5"),
6 => Some("switch_tab_6"),
7 => Some("switch_tab_7"),
8 => Some("switch_tab_8"),
9 => Some("switch_tab_9"),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Modifier {
Primary,
Alt,
Shift,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Binding {
pub action: String,
pub mods: Vec<Modifier>,
pub key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Bindings {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub bindings: Vec<Binding>,
}
pub fn default_bindings() -> Vec<Binding> {
let prefix = || vec![Modifier::Primary, Modifier::Alt, Modifier::Shift];
let mut v = vec![
Binding { action: action_id::SEND_CTRL_ALT_DEL.into(), mods: prefix(), key: "delete".into() },
Binding { action: action_id::TOGGLE_FULLSCREEN.into(), mods: prefix(), key: "enter".into() },
Binding { action: action_id::SWITCH_DISPLAY_NEXT.into(), mods: prefix(), key: "arrow_right".into() },
Binding { action: action_id::SWITCH_DISPLAY_PREV.into(), mods: prefix(), key: "arrow_left".into() },
Binding { action: action_id::SCREENSHOT.into(), mods: prefix(), key: "p".into() },
];
for n in 1..=9u8 {
if let Some(action) = switch_tab_action_id(n) {
v.push(Binding {
action: action.into(),
mods: prefix(),
key: format!("digit{n}"),
});
}
}
v
}
/// Match a normalized (key, modifiers) pair against the given bindings.
/// Returns the matched action ID, or None.
pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
if !b.enabled {
return None;
}
for binding in &b.bindings {
if binding.key == key && mods_equal(&binding.mods, mods) {
return Some(binding.action.as_str());
}
}
None
}
pub fn normalize_modifiers(alt: bool, ctrl: bool, shift: bool, command: bool) -> Vec<Modifier> {
let mut v = Vec::new();
let primary = if cfg!(target_os = "macos") { command } else { ctrl };
if primary { v.push(Modifier::Primary); }
if alt { v.push(Modifier::Alt); }
if shift { v.push(Modifier::Shift); }
v
}
/// Map an rdev::Event to a string key name, matching the storage schema.
/// Returns None for events we don't intercept (modifier-only presses, releases, etc.).
pub fn event_to_key_name(event: &rdev::Event) -> Option<String> {
use rdev::{EventType, Key};
let key = match event.event_type {
EventType::KeyPress(k) => k,
_ => return None,
};
Some(match key {
Key::Delete => "delete".into(),
Key::Return => "enter".into(),
Key::LeftArrow => "arrow_left".into(),
Key::RightArrow => "arrow_right".into(),
Key::UpArrow => "arrow_up".into(),
Key::DownArrow => "arrow_down".into(),
Key::KeyA => "a".into(),
Key::KeyB => "b".into(),
Key::KeyC => "c".into(),
Key::KeyD => "d".into(),
Key::KeyE => "e".into(),
Key::KeyF => "f".into(),
Key::KeyG => "g".into(),
Key::KeyH => "h".into(),
Key::KeyI => "i".into(),
Key::KeyJ => "j".into(),
Key::KeyK => "k".into(),
Key::KeyL => "l".into(),
Key::KeyM => "m".into(),
Key::KeyN => "n".into(),
Key::KeyO => "o".into(),
Key::KeyP => "p".into(),
Key::KeyQ => "q".into(),
Key::KeyR => "r".into(),
Key::KeyS => "s".into(),
Key::KeyT => "t".into(),
Key::KeyU => "u".into(),
Key::KeyV => "v".into(),
Key::KeyW => "w".into(),
Key::KeyX => "x".into(),
Key::KeyY => "y".into(),
Key::KeyZ => "z".into(),
Key::Num1 => "digit1".into(),
Key::Num2 => "digit2".into(),
Key::Num3 => "digit3".into(),
Key::Num4 => "digit4".into(),
Key::Num5 => "digit5".into(),
Key::Num6 => "digit6".into(),
Key::Num7 => "digit7".into(),
Key::Num8 => "digit8".into(),
Key::Num9 => "digit9".into(),
_ => return None,
})
}
/// Read keyboard-shortcut bindings from `LocalConfig` and refresh the cache.
///
/// Empty or invalid JSON falls back to `Bindings::default()` (disabled, no
/// bindings). Call this once at startup and again whenever the config is
/// written.
pub fn reload_from_config() {
let raw = hbb_common::config::LocalConfig::get_option(LOCAL_CONFIG_KEY);
let parsed = if raw.is_empty() {
Bindings::default()
} else {
serde_json::from_str(&raw).unwrap_or_default()
};
if let Ok(mut w) = CACHE.write() {
*w = Arc::new(parsed);
}
}
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
/// safe to call on every keystroke.
pub fn current() -> Arc<Bindings> {
CACHE
.read()
.map(|b| Arc::clone(&b))
.unwrap_or_else(|_| Arc::new(Bindings::default()))
}
/// Match an `rdev::Event` against the cached bindings. Returns the matched
/// action id, or `None` if no binding fires. The Flutter side ignores unknown
/// action ids (logged as "no handler"), so no whitelist check is needed here.
pub fn match_event(event: &rdev::Event) -> Option<String> {
let bindings = current();
if !bindings.enabled {
return None;
}
let key_name = event_to_key_name(event)?;
let (alt, ctrl, shift, command) =
crate::keyboard::client::get_modifiers_state(false, false, false, false);
let mods = normalize_modifiers(alt, ctrl, shift, command);
match_normalized(&key_name, &mods, &bindings).map(str::to_owned)
}
fn mods_bits(m: &[Modifier]) -> u8 {
let mut bits = 0u8;
for x in m {
bits |= match x {
Modifier::Primary => 1,
Modifier::Alt => 2,
Modifier::Shift => 4,
};
}
bits
}
fn mods_equal(a: &[Modifier], b: &[Modifier]) -> bool {
mods_bits(a) == mods_bits(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bindings_round_trip_json() {
let json = r#"{
"enabled": true,
"bindings": [
{"action": "send_ctrl_alt_del", "mods": ["primary","alt","shift"], "key": "delete"},
{"action": "toggle_fullscreen", "mods": ["primary","alt","shift"], "key": "enter"}
]
}"#;
let parsed: Bindings = serde_json::from_str(json).expect("parse");
assert!(parsed.enabled);
assert_eq!(parsed.bindings.len(), 2);
assert_eq!(parsed.bindings[0].action, "send_ctrl_alt_del");
assert_eq!(parsed.bindings[0].key, "delete");
let serialized = serde_json::to_string(&parsed).expect("serialize");
let reparsed: Bindings = serde_json::from_str(&serialized).expect("reparse");
assert_eq!(parsed, reparsed);
}
#[test]
fn defaults_match_design_doc() {
let defaults = default_bindings();
let actions: Vec<&str> = defaults.iter().map(|b| b.action.as_str()).collect();
assert!(actions.contains(&action_id::SEND_CTRL_ALT_DEL));
assert!(actions.contains(&action_id::TOGGLE_FULLSCREEN));
assert!(actions.contains(&action_id::SWITCH_DISPLAY_NEXT));
assert!(actions.contains(&action_id::SWITCH_DISPLAY_PREV));
assert!(actions.contains(&action_id::SCREENSHOT));
assert!(actions.contains(&"switch_tab_1"));
assert!(actions.contains(&"switch_tab_9"));
// every default binding includes the three-modifier prefix
for b in &defaults {
assert!(b.mods.contains(&Modifier::Primary));
assert!(b.mods.contains(&Modifier::Alt));
assert!(b.mods.contains(&Modifier::Shift));
}
}
fn match_for_test<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
match_normalized(key, mods, b)
}
#[test]
fn match_returns_none_when_disabled() {
let bindings = Bindings { enabled: false, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_screenshot_when_enabled() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, Some(action_id::SCREENSHOT));
}
#[test]
fn match_returns_none_when_modifiers_partial() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
// missing Shift
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_does_not_fire_on_extra_unbound_keys() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("z", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_handles_duplicate_modifiers_in_input() {
// A user-edited config could contain duplicate modifiers; the matcher must
// treat the modifier list as a set, not a multiset.
let bindings = Bindings {
enabled: true,
bindings: vec![Binding {
action: "x".into(),
mods: vec![Modifier::Primary, Modifier::Alt],
key: "a".into(),
}],
};
// Caller passes Primary twice — must not match a binding with Primary+Alt.
assert_eq!(
match_normalized("a", &[Modifier::Primary, Modifier::Primary], &bindings),
None,
);
// Caller passes Primary+Alt with one duplicate — should still match.
assert_eq!(
match_normalized("a", &[Modifier::Primary, Modifier::Alt, Modifier::Alt], &bindings),
Some("x"),
);
}
#[test]
fn modifier_normalization_primary_resolves_per_os() {
// On Win/Linux: pressing Ctrl satisfies Primary
let mods = normalize_modifiers(/*alt=*/true, /*ctrl=*/true, /*shift=*/true, /*command=*/false);
if cfg!(target_os = "macos") {
// On macOS Ctrl is NOT primary
assert!(!mods.contains(&Modifier::Primary));
} else {
assert!(mods.contains(&Modifier::Primary));
}
assert!(mods.contains(&Modifier::Alt));
assert!(mods.contains(&Modifier::Shift));
}
#[test]
fn modifier_normalization_command_is_primary_on_mac() {
let mods = normalize_modifiers(true, false, true, /*command=*/true);
if cfg!(target_os = "macos") {
assert!(mods.contains(&Modifier::Primary));
} else {
// On Win/Linux Command/Meta is NOT primary
assert!(!mods.contains(&Modifier::Primary));
}
}
#[test]
fn reload_handles_missing_and_invalid_json() {
// empty (no value set) → defaults
hbb_common::config::LocalConfig::set_option(LOCAL_CONFIG_KEY.into(), String::new());
reload_from_config();
let b = current();
assert!(!b.enabled);
assert!(b.bindings.is_empty());
// invalid JSON → defaults (no panic)
hbb_common::config::LocalConfig::set_option(LOCAL_CONFIG_KEY.into(), "not json".into());
reload_from_config();
let b = current();
assert!(!b.enabled);
}
}

View File

@@ -16,8 +16,10 @@ mod es;
mod et;
mod eu;
mod fa;
mod gu;
mod fr;
mod he;
mod hi;
mod hr;
mod hu;
mod id;
@@ -47,6 +49,7 @@ mod vi;
mod ta;
mod ge;
mod fi;
mod ml;
pub const LANGS: &[(&str, &str)] = &[
("en", "English"),
@@ -95,6 +98,9 @@ pub const LANGS: &[(&str, &str)] = &[
("ta", "தமிழ்"),
("ge", "ქართული"),
("fi", "Suomi"),
("ml", "മലയാളം"),
("hi", "हिंदी"),
("gu", "ગુજરાતી"),
];
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -173,6 +179,9 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"sc" => sc::T.deref(),
"ta" => ta::T.deref(),
"ge" => ge::T.deref(),
"ml" => ml::T.deref(),
"hi" => hi::T.deref(),
"gu" => gu::T.deref(),
_ => en::T.deref(),
};
let (name, placeholder_value) = extract_placeholder(&name);

View File

@@ -729,19 +729,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"),
("input note here", "أدخل الملاحظة هنا"),
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Show terminal extra keys", "إظهار مفاتيح إضافية في الطرفية"),
("Relative mouse mode", "وضع الماوس النسبي"),
("rel-mouse-not-supported-peer-tip", "وضع الماوس النسبي غير مدعوم على الجهاز الآخر"),
("rel-mouse-not-ready-tip", "وضع الماوس النسبي غير جاهز"),
("rel-mouse-lock-failed-tip", "فشل قفل الماوس النسبي"),
("rel-mouse-exit-{}-tip", "للخروج من وضع الماوس النسبي اضغط على {}"),
("rel-mouse-permission-lost-tip", "تم فقدان إذن الماوس النسبي"),
("Changelog", "سجل التغييرات"),
("keep-awake-during-outgoing-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الصادرة"),
("keep-awake-during-incoming-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الواردة"),
("Continue with {}", "متابعة مع {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Display Name", "اسم العرض"),
("password-hidden-tip", "كلمة المرور مخفية"),
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
].iter().cloned().collect();
}

File diff suppressed because it is too large Load Diff

View File

@@ -743,5 +743,44 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "显示名称"),
("password-hidden-tip", "永久密码已设置(已隐藏)"),
("preset-password-in-use-tip", "当前使用预设密码"),
("Keyboard Shortcuts", "键盘快捷键"),
("Configure shortcuts...", "配置快捷键..."),
("Enable keyboard shortcuts in remote session", "在远程会话中启用键盘快捷键"),
("shortcut-page-description", "启用后,列出的组合键将在本地触发会话操作,而不会发送到远程端。所有快捷键必须包含 Ctrl+Alt+ShiftmacOS 上为 Cmd+Option+Shift以避免与正常输入冲突。"),
("Reset to defaults", "恢复默认设置"),
("shortcut-reset-confirm-tip", "这将以默认快捷键替换所有当前绑定。是否继续?"),
("Session Control", "会话控制"),
("Toggle Fullscreen", "切换全屏"),
("Switch to next display", "切换到下一个显示器"),
("Switch to previous display", "切换到上一个显示器"),
("View Mode 1:1", "原始大小"),
("View Mode Shrink", "缩小"),
("View Mode Stretch", "拉伸"),
("Take Screenshot", "截图"),
("Toggle Audio", "切换音频"),
("Toggle Privacy Mode", "切换隐私模式"),
("Toggle Recording", "切换录制"),
("Toggle Block User Input", "切换屏蔽用户输入"),
("Switch Tab 1", "切换到第 1 个标签"),
("Switch Tab 2", "切换到第 2 个标签"),
("Switch Tab 3", "切换到第 3 个标签"),
("Switch Tab 4", "切换到第 4 个标签"),
("Switch Tab 5", "切换到第 5 个标签"),
("Switch Tab 6", "切换到第 6 个标签"),
("Switch Tab 7", "切换到第 7 个标签"),
("Switch Tab 8", "切换到第 8 个标签"),
("Switch Tab 9", "切换到第 9 个标签"),
("Edit", "编辑"),
("Save", "保存"),
("Set Shortcut", "设置快捷键"),
("shortcut-recording-instruction", "请按下您想使用的组合键。"),
("shortcut-recording-press-keys-tip", "请按下组合键..."),
("shortcut-must-include-prefix", "必须包含"),
("shortcut-already-bound-to", "已绑定到"),
("Replace", "替换"),
("Valid", "有效"),
("shortcut-mobile-physical-keyboard-tip", "录制需要使用物理键盘,不支持软键盘。"),
("On", ""),
("Off", ""),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
("Continue with {}", "Fortfahren mit {}"),
("Display Name", "Anzeigename"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
].iter().cloned().collect();
}

View File

@@ -274,5 +274,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
("password-hidden-tip", "Permanent password is set (hidden)."),
("preset-password-in-use-tip", "Preset password is currently in use."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", "When enabled, listed key combinations trigger session actions locally instead of being sent to the remote. All bindings must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS) to avoid conflicts with normal typing."),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", "This will replace all current bindings with the default set. Continue?"),
("Session Control", ""),
("Display", ""),
("Other", ""),
("Toggle Fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("View Mode 1:1", ""),
("View Mode Shrink", ""),
("View Mode Stretch", ""),
("Take Screenshot", ""),
("Toggle Audio", ""),
("Toggle Privacy Mode", ""),
("Toggle Recording", ""),
("Toggle Block User Input", ""),
("Switch Tab 1", ""),
("Switch Tab 2", ""),
("Switch Tab 3", ""),
("Switch Tab 4", ""),
("Switch Tab 5", ""),
("Switch Tab 6", ""),
("Switch Tab 7", ""),
("Switch Tab 8", ""),
("Switch Tab 9", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", "Press the key combination you want to use."),
("shortcut-recording-press-keys-tip", "Press a key combination..."),
("shortcut-must-include-prefix", "Must include"),
("shortcut-already-bound-to", "Already bound to"),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."),
("Clear", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Maintenir lécran allumé lors des sessions entrantes"),
("Continue with {}", "Continuer avec {}"),
("Display Name", "Nom daffichage"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."),
("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."),
].iter().cloned().collect();
}

746
src/lang/gu.rs Normal file
View File

@@ -0,0 +1,746 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "સ્થિતિ"),
("Your Desktop", "તમારું ડેસ્કટોપ"),
("desk_tip", "તમારું ડેસ્કટોપ આ ID અને પાસવર્ડ દ્વારા એક્સેસ કરી શકાય છે."),
("Password", "પાસવર્ડ"),
("Ready", "તૈયાર"),
("Established", "સ્થાપિત"),
("connecting_status", "નેટવર્ક સાથે જોડાઈ રહ્યું છે..."),
("Enable service", "સેવા સક્ષમ કરો"),
("Start service", "સેવા શરૂ કરો"),
("Service is running", "સેવા કાર્યરત છે"),
("Service is not running", "સેવા કાર્યરત નથી"),
("not_ready_status", "તૈયાર નથી. કૃપા કરીને તમારું કનેક્શન તપાસો"),
("Control Remote Desktop", "રિમોટ ડેસ્કટોપ નિયંત્રિત કરો"),
("Transfer file", "ફાઇલ ટ્રાન્સફર"),
("Connect", "કનેક્ટ કરો"),
("Recent sessions", "તાજેતરના સત્રો"),
("Address book", "એડ્રેસ બુક"),
("Confirmation", "પુષ્ટિકરણ"),
("TCP tunneling", "TCP ટનલિંગ"),
("Remove", "દૂર કરો"),
("Refresh random password", "રેન્ડમ પાસવર્ડ બદલો"),
("Set your own password", "તમારો પોતાનો પાસવર્ડ સેટ કરો"),
("Enable keyboard/mouse", "કીબોર્ડ/માઉસ સક્ષમ કરો"),
("Enable clipboard", "ક્લિપબોર્ડ સક્ષમ કરો"),
("Enable file transfer", "ફાઇલ ટ્રાન્સફર સક્ષમ કરો"),
("Enable TCP tunneling", "TCP ટનલિંગ સક્ષમ કરો"),
("IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગ"),
("ID/Relay Server", "ID/રિલે સર્વર"),
("Import server config", "સર્વર કોન્ફિગ ઈમ્પોર્ટ કરો"),
("Export Server Config", "સર્વર કોન્ફિગ એક્સપોર્ટ કરો"),
("Import server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક ઈમ્પોર્ટ થયું"),
("Export server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક એક્સપોર્ટ થયું"),
("Invalid server configuration", "અમાન્ય સર્વર કોન્ફિગરેશન"),
("Clipboard is empty", "ક્લિપબોર્ડ ખાલી છે"),
("Stop service", "સેવા બંધ કરો"),
("Change ID", "ID બદલો"),
("Your new ID", "તમારું નવું ID"),
("length %min% to %max%", "લંબાઈ %min% થી %max% સુધી"),
("starts with a letter", "અક્ષરથી શરૂ થાય છે"),
("allowed characters", "માન્ય અક્ષરો"),
("id_change_tip", "ID બદલ્યા પછી વર્તમાન કનેક્શન તૂટી જશે."),
("Website", "વેબસાઇટ"),
("About", "વિશે"),
("Slogan_tip", "વધુ સારા અનુભવ માટે બનાવેલ રિમોટ ડેસ્કટોપ સોફ્ટવેર"),
("Privacy Statement", "ગોપનીયતા નિવેદન"),
("Mute", "મ્યૂટ કરો"),
("Build Date", "બિલ્ડ તારીખ"),
("Version", "સંસ્કરણ (Version)"),
("Home", "હોમ"),
("Audio Input", "ઓડિયો ઇનપુટ"),
("Enhancements", "વધારાની સુવિધાઓ"),
("Hardware Codec", "હાર્ડવેર કોડેક"),
("Adaptive bitrate", "એડેપ્ટિવ બિટરેટ"),
("ID Server", "ID સર્વર"),
("Relay Server", "રિલે સર્વર"),
("API Server", "API સર્વર"),
("invalid_http", "અમાન્ય HTTP લિંક"),
("Invalid IP", "અમાન્ય IP"),
("Invalid format", "અમાન્ય ફોર્મેટ"),
("server_not_support", "સર્વર દ્વારા સમર્થિત નથી"),
("Not available", "ઉપલબ્ધ નથી"),
("Too frequent", "ખૂબ વારંવાર"),
("Cancel", "રદ કરો"),
("Skip", "રહેવા દો (Skip)"),
("Close", "બંધ કરો"),
("Retry", "ફરી પ્રયાસ કરો"),
("OK", "બરાબર"),
("Password Required", "પાસવર્ડ જરૂરી છે"),
("Please enter your password", "કૃપા કરીને તમારો પાસવર્ડ દાખલ કરો"),
("Remember password", "પાસવર્ડ યાદ રાખો"),
("Wrong Password", "ખોટો પાસવર્ડ"),
("Do you want to enter again?", "શું તમે ફરીથી દાખલ કરવા માંગો છો?"),
("Connection Error", "કનેક્શન ભૂલ"),
("Error", "ભૂલ"),
("Reset by the peer", "સામેના છેડેથી રિસેટ કરવામાં આવ્યું"),
("Connecting...", "જોડાઈ રહ્યું છે..."),
("Connection in progress. Please wait.", "કનેક્શન ચાલુ છે. કૃપા કરીને રાહ જુઓ."),
("Please try 1 minute later", "કૃપા કરીને 1 મિનિટ પછી ફરી પ્રયાસ કરો"),
("Login Error", "લોગિન ભૂલ"),
("Successful", "સફળ"),
("Connected, waiting for image...", "જોડાયેલ, ઇમેજની રાહ જોવાય છે..."),
("Name", "નામ"),
("Type", "પ્રકાર"),
("Modified", "સુધારેલ"),
("Size", "કદ (Size)"),
("Show Hidden Files", "છુપાયેલી ફાઇલો બતાવો"),
("Receive", "મેળવો"),
("Send", "મોકલો"),
("Refresh File", "ફાઇલ રિફ્રેશ કરો"),
("Local", "લોકલ"),
("Remote", "રિમોટ"),
("Remote Computer", "રિમોટ કોમ્પ્યુટર"),
("Local Computer", "લોકલ કોમ્પ્યુટર"),
("Confirm Delete", "કાઢી નાખવાની પુષ્ટિ કરો"),
("Delete", "કાઢી નાખો"),
("Properties", "ગુણધર્મો (Properties)"),
("Multi Select", "બહુ-પસંદગી"),
("Select All", "બધું પસંદ કરો"),
("Unselect All", "બધું નાપસંદ કરો"),
("Empty Directory", "ખાલી ડિરેક્ટરી"),
("Not an empty directory", "ડિરેક્ટરી ખાલી નથી"),
("Are you sure you want to delete this file?", "શું તમે ખરેખર આ ફાઇલ કાઢી નાખવા માંગો છો?"),
("Are you sure you want to delete this empty directory?", "શું તમે ખરેખર આ ખાલી ડિરેક્ટરી કાઢી નાખવા માંગો છો?"),
("Are you sure you want to delete the file of this directory?", "શું તમે ખરેખર આ ડિરેક્ટરીની ફાઇલ કાઢી નાખવા માંગો છો?"),
("Do this for all conflicts", "તમામ વિવાદો માટે આ કરો"),
("This is irreversible!", "આ બદલી શકાશે નહીં!"),
("Deleting", "કાઢી નાખવામાં આવી રહ્યું છે"),
("files", "ફાઇલો"),
("Waiting", "રાહ જુઓ"),
("Finished", "પૂરું થયું"),
("Speed", "ગતિ"),
("Custom Image Quality", "કસ્ટમ ઇમેજ ગુણવત્તા"),
("Privacy mode", "પ્રાઇવસી મોડ"),
("Block user input", "યુઝર ઇનપુટ બ્લોક કરો"),
("Unblock user input", "યુઝર ઇનપુટ અનબ્લોક કરો"),
("Adjust Window", "વિન્ડો એડજસ્ટ કરો"),
("Original", "મૂળ (Original)"),
("Shrink", "સંકોચો (Shrink)"),
("Stretch", "ખેંચો (Stretch)"),
("Scrollbar", "સ્ક્રોલબાર"),
("ScrollAuto", "ઓટો સ્ક્રોલ"),
("Good image quality", "સારી ઇમેજ ગુણવત્તા"),
("Balanced", "સંતુલિત"),
("Optimize reaction time", "પ્રતિક્રિયા સમય શ્રેષ્ઠ બનાવો"),
("Custom", "કસ્ટમ"),
("Show remote cursor", "રિમોટ કર્સર બતાવો"),
("Show quality monitor", "ક્વોલિટી મોનિટર બતાવો"),
("Disable clipboard", "ક્લિપબોર્ડ અક્ષમ કરો"),
("Lock after session end", "સત્ર સમાપ્ત થયા પછી લોક કરો"),
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del દાખલ કરો"),
("Insert Lock", "લોક દાખલ કરો"),
("Refresh", "રિફ્રેશ કરો"),
("ID does not exist", "ID અસ્તિત્વમાં નથી"),
("Failed to connect to rendezvous server", "Rendezvous સર્વર સાથે જોડવામાં નિષ્ફળ"),
("Please try later", "કૃપા કરીને પછી પ્રયાસ કરો"),
("Remote desktop is offline", "રિમોટ ડેસ્કટોપ ઓફલાઇન છે"),
("Key mismatch", "કી મેળ ખાતી નથી"),
("Timeout", "સમય સમાપ્ત"),
("Failed to connect to relay server", "રિલે સર્વર સાથે જોડવામાં નિષ્ફળ"),
("Failed to connect via rendezvous server", "Rendezvous સર્વર દ્વારા જોડવામાં નિષ્ફળ"),
("Failed to connect via relay server", "રિલે સર્વર દ્વારા જોડવામાં નિષ્ફળ"),
("Failed to make direct connection to remote desktop", "રિમોટ ડેસ્કટોપ સાથે સીધું જોડાણ કરવામાં નિષ્ફળ"),
("Set Password", "પાસવર્ડ સેટ કરો"),
("OS Password", "OS પાસવર્ડ"),
("install_tip", "શ્રેષ્ઠ પ્રદર્શન માટે, કૃપા કરીને ઇન્સ્ટોલ કરો."),
("Click to upgrade", "અપગ્રેડ કરવા માટે ક્લિક કરો"),
("Configure", "કોન્ફિગર કરો"),
("config_acc", "એક્સેસિબિલિટી કોન્ફિગર કરો"),
("config_screen", "સ્ક્રીન કોન્ફિગર કરો"),
("Installing ...", "ઇન્સ્ટોલ થઈ રહ્યું છે..."),
("Install", "ઇન્સ્ટોલ કરો"),
("Installation", "ઇન્સ્ટોલેશન"),
("Installation Path", "ઇન્સ્ટોલેશન પાથ"),
("Create start menu shortcuts", "સ્ટાર્ટ મેનૂ શોર્ટકટ બનાવો"),
("Create desktop icon", "ડેસ્કટોપ આઇકોન બનાવો"),
("agreement_tip", "ઇન્સ્ટોલ કરીને તમે લાયસન્સ કરાર સ્વીકારો છો."),
("Accept and Install", "સ્વીકારો અને ઇન્સ્ટોલ કરો"),
("End-user license agreement", "અંતિમ વપરાશકર્તા લાયસન્સ કરાર"),
("Generating ...", "જનરેટ થઈ રહ્યું છે..."),
("Your installation is lower version.", "તમારું ઇન્સ્ટોલેશન જૂનું સંસ્કરણ છે."),
("not_close_tcp_tip", "ટનલનો ઉપયોગ કરતી વખતે આ વિન્ડો બંધ કરશો નહીં."),
("Listening ...", "સાંભળી રહ્યું છે..."),
("Remote Host", "રિમોટ હોસ્ટ"),
("Remote Port", "રિમોટ પોર્ટ"),
("Action", "ક્રિયા"),
("Add", "ઉમેરો"),
("Local Port", "લોકલ પોર્ટ"),
("Local Address", "લોકલ સરનામું"),
("Change Local Port", "લોકલ પોર્ટ બદલો"),
("setup_server_tip", "ઝડપી કનેક્શન માટે તમારું પોતાનું સર્વર સેટ કરો"),
("Too short, at least 6 characters.", "ખૂબ ટૂંકું, ઓછામાં ઓછા 6 અક્ષરો હોવા જોઈએ."),
("The confirmation is not identical.", "પુષ્ટિકરણ સરખું નથી."),
("Permissions", "પરવાનગીઓ"),
("Accept", "સ્વીકારો"),
("Dismiss", "ખારીજ કરો"),
("Disconnect", "ડિસ્કનેક્ટ કરો"),
("Enable file copy and paste", "ફાઇલ કોપી અને પેસ્ટ સક્ષમ કરો"),
("Connected", "જોડાયેલ"),
("Direct and encrypted connection", "સીધું અને એન્ક્રિપ્ટેડ કનેક્શન"),
("Relayed and encrypted connection", "રિલે અને એન્ક્રિપ્ટેડ કનેક્શન"),
("Direct and unencrypted connection", "સીધું અને અનએન્ક્રિપ્ટેડ કનેક્શન"),
("Relayed and unencrypted connection", "રિલે અને અનએન્ક્રિપ્ટેડ કનેક્શન"),
("Enter Remote ID", "રિમોટ ID દાખલ કરો"),
("Enter your password", "તમારો પાસવર્ડ દાખલ કરો"),
("Logging in...", "લોગિન થઈ રહ્યું છે..."),
("Enable RDP session sharing", "RDP સત્ર શેરિંગ સક્ષમ કરો"),
("Auto Login", "ઓટો લોગિન"),
("Enable direct IP access", "સીધું IP એક્સેસ સક્ષમ કરો"),
("Rename", "નામ બદલો"),
("Space", "જગ્યા (Space)"),
("Create desktop shortcut", "ડેસ્કટોપ શોર્ટકટ બનાવો"),
("Change Path", "પાથ બદલો"),
("Create Folder", "ફોલ્ડર બનાવો"),
("Please enter the folder name", "કૃપા કરીને ફોલ્ડરનું નામ દાખલ કરો"),
("Fix it", "તેને ઠીક કરો"),
("Warning", "ચેતવણી"),
("Login screen using Wayland is not supported", "Wayland ઉપયોગ કરતી લોગિન સ્ક્રીન સમર્થિત નથી"),
("Reboot required", "રિબૂટ જરૂરી છે"),
("Unsupported display server", "અસમર્થિત ડિસ્પ્લે સર્વર"),
("x11 expected", "x11 અપેક્ષિત છે"),
("Port", "પોર્ટ"),
("Settings", "સેટિંગ્સ"),
("Username", "વપરાશકર્તા નામ"),
("Invalid port", "અમાન્ય પોર્ટ"),
("Closed manually by the peer", "સામેથી મેન્યુઅલી બંધ કરવામાં આવ્યું"),
("Enable remote configuration modification", "રિમોટ કોન્ફિગરેશન ફેરફાર સક્ષમ કરો"),
("Run without install", "ઇન્સ્ટોલ કર્યા વગર ચલાવો"),
("Connect via relay", "રિલે દ્વારા કનેક્ટ કરો"),
("Always connect via relay", "હંમેશા રિલે દ્વારા કનેક્ટ કરો"),
("whitelist_tip", "માત્ર વ્હાઇટલિસ્ટ કરેલ IP જ મને એક્સેસ કરી શકે છે"),
("Login", "લોગિન"),
("Verify", "ચકાસો"),
("Remember me", "મને યાદ રાખો"),
("Trust this device", "આ ઉપકરણ પર વિશ્વાસ કરો"),
("Verification code", "વેરિફિકેશન કોડ"),
("verification_tip", "વેરિફિકેશન કોડ તમારા ઇમેઇલ પર મોકલવામાં આવ્યો છે"),
("Logout", "લોગઆઉટ"),
("Tags", "ટેગ્સ"),
("Search ID", "ID શોધો"),
("whitelist_sep", "અલ્પવિરામ, અર્ધવિરામ અથવા સ્પેસ દ્વારા અલગ કરો"),
("Add ID", "ID ઉમેરો"),
("Add Tag", "ટેગ ઉમેરો"),
("Unselect all tags", "તમામ ટેગ નાપસંદ કરો"),
("Network error", "નેટવર્ક ભૂલ"),
("Username missed", "વપરાશકર્તા નામ બાકી છે"),
("Password missed", "પાસવર્ડ બાકી છે"),
("Wrong credentials", "ખોટી વિગતો"),
("The verification code is incorrect or has expired", "વેરિફિકેશન કોડ ખોટો છે અથવા તેની મર્યાદા પૂરી થઈ ગઈ છે"),
("Edit Tag", "ટેગ સુધારો"),
("Forget Password", "પાસવર્ડ ભૂલી ગયા"),
("Favorites", "પસંદગીના"),
("Add to Favorites", "પસંદગીમાં ઉમેરો"),
("Remove from Favorites", "પસંદગીમાંથી દૂર કરો"),
("Empty", "ખાલી"),
("Invalid folder name", "અમાન્ય ફોલ્ડર નામ"),
("Socks5 Proxy", "Socks5 પ્રોક્સી"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) પ્રોક્સી"),
("Discovered", "શોધાયેલ"),
("install_daemon_tip", "બૂટ વખતે શરૂ કરવા માટે સેવા ઇન્સ્ટોલ કરો"),
("Remote ID", "રિમોટ ID"),
("Paste", "પેસ્ટ કરો"),
("Paste here?", "અહીં પેસ્ટ કરવું છે?"),
("Are you sure to close the connection?", "શું તમે ખરેખર કનેક્શન બંધ કરવા માંગો છો?"),
("Download new version", "નવું સંસ્કરણ ડાઉનલોડ કરો"),
("Touch mode", "ટચ મોડ"),
("Mouse mode", "માઉસ મોડ"),
("One-Finger Tap", "એક આંગળીથી ટેપ"),
("Left Mouse", "ડાબું માઉસ બટન"),
("One-Long Tap", "એક લાંબો ટેપ"),
("Two-Finger Tap", "બે આંગળીથી ટેપ"),
("Right Mouse", "જમણું માઉસ બટન"),
("One-Finger Move", "એક આંગળીથી હલનચલન"),
("Double Tap & Move", "ડબલ ટેપ અને હલનચલન"),
("Mouse Drag", "માઉસ ડ્રેગ"),
("Three-Finger vertically", "ત્રણ આંગળી ઊભી રીતે"),
("Mouse Wheel", "માઉસ વ્હીલ"),
("Two-Finger Move", "બે આંગળીથી હલનચલન"),
("Canvas Move", "કેનવાસ ખસેડો"),
("Pinch to Zoom", "ઝૂમ કરવા માટે પિંચ કરો"),
("Canvas Zoom", "કેનવાસ ઝૂમ"),
("Reset canvas", "કેનવાસ રિસેટ કરો"),
("No permission of file transfer", "ફાઇલ ટ્રાન્સફરની પરવાનગી નથી"),
("Note", "નોંધ"),
("Connection", "કનેક્શન"),
("Share screen", "સ્ક્રીન શેર કરો"),
("Chat", "ચેટ"),
("Total", "કુલ"),
("items", "વસ્તુઓ"),
("Selected", "પસંદ કરેલ"),
("Screen Capture", "સ્ક્રીન કેપ્ચર"),
("Input Control", "ઇનપુટ નિયંત્રણ"),
("Audio Capture", "ઓડિયો કેપ્ચર"),
("Do you accept?", "શું તમે સ્વીકારો છો?"),
("Open System Setting", "સિસ્ટમ સેટિંગ ખોલો"),
("How to get Android input permission?", "Android ઇનપુટ પરવાનગી કેવી રીતે મેળવવી?"),
("android_input_permission_tip1", "ઇનપુટ પરવાનગી મેળવવા માટે એક્સેસિબિલિટી સેવા સક્ષમ કરો."),
("android_input_permission_tip2", "કૃપા કરીને સેટિંગ્સમાં RustDesk શોધો અને તેને ચાલુ કરો."),
("android_new_connection_tip", "નવો કંટ્રોલ વિનંતી પ્રાપ્ત થઈ છે."),
("android_service_will_start_tip", "સ્ક્રીન કેપ્ચર ચાલુ કરવાથી સેવા આપમેળે શરૂ થશે."),
("android_stop_service_tip", "સેવા બંધ કરવાથી તમામ કનેક્શન બંધ થઈ જશે."),
("android_version_audio_tip", "ઓડિયો કેપ્ચર માત્ર Android 10 કે તેથી ઉપરના વર્ઝનમાં ઉપલબ્ધ છે."),
("android_start_service_tip", "સ્ક્રીન શેરિંગ સેવા શરૂ કરવા ક્લિક કરો."),
("android_permission_may_not_change_tip", "પરવાનગીઓ પછીથી બદલી શકાશે નહીં, કૃપા કરીને કાળજીપૂર્વક પસંદ કરો."),
("Account", "ખાતું"),
("Overwrite", "ઓવરરાઇટ કરો"),
("This file exists, skip or overwrite this file?", "આ ફાઇલ અસ્તિત્વમાં છે, રહેવા દેવી છે કે ઓવરરાઇટ કરવી છે?"),
("Quit", "બહાર નીકળો"),
("Help", "મદદ"),
("Failed", "નિષ્ફળ"),
("Succeeded", "સફળ"),
("Someone turns on privacy mode, exit", "કોઈએ પ્રાઇવસી મોડ ચાલુ કર્યો છે, બહાર નીકળો"),
("Unsupported", "અસમર્થિત"),
("Peer denied", "સામેથી નકારવામાં આવ્યું"),
("Please install plugins", "કૃપા કરીને પ્લગઇન્સ ઇન્સ્ટોલ કરો"),
("Peer exit", "સામેથી કોઈ બહાર નીકળી ગયું"),
("Failed to turn off", "બંધ કરવામાં નિષ્ફળ"),
("Turned off", "બંધ કરવામાં આવ્યું"),
("Language", "ભાષા"),
("Keep RustDesk background service", "RustDesk બેકગ્રાઉન્ડ સેવા ચાલુ રાખો"),
("Ignore Battery Optimizations", "બેટરી ઓપ્ટિમાઇઝેશન અવગણો"),
("android_open_battery_optimizations_tip", "ડિસ્કનેક્શન ટાળવા માટે બેટરી ઓપ્ટિમાઇઝેશન સેટિંગ ખોલો"),
("Start on boot", "બૂટ પર શરૂ કરો"),
("Start the screen sharing service on boot, requires special permissions", "બૂટ પર સ્ક્રીન શેરિંગ શરૂ કરો, ખાસ પરવાનગીની જરૂર છે"),
("Connection not allowed", "કનેક્શનની પરવાનગી નથી"),
("Legacy mode", "લેગસી મોડ"),
("Map mode", "મેપ મોડ"),
("Translate mode", "અનુવાદ મોડ"),
("Use permanent password", "કાયમી પાસવર્ડનો ઉપયોગ કરો"),
("Use both passwords", "બંને પાસવર્ડનો ઉપયોગ કરો"),
("Set permanent password", "કાયમી પાસવર્ડ સેટ કરો"),
("Enable remote restart", "રિમોટ રિસ્ટાર્ટ સક્ષમ કરો"),
("Restart remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ કરો"),
("Are you sure you want to restart", "શું તમે ખરેખર રિસ્ટાર્ટ કરવા માંગો છો?"),
("Restarting remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે"),
("remote_restarting_tip", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે, કૃપા કરીને રાહ જુઓ..."),
("Copied", "કોપી થઈ ગયું"),
("Exit Fullscreen", "ફુલસ્ક્રીનમાંથી બહાર નીકળો"),
("Fullscreen", "ફુલસ્ક્રીન"),
("Mobile Actions", "મોબાઇલ ક્રિયાઓ"),
("Select Monitor", "મોનિટર પસંદ કરો"),
("Control Actions", "નિયંત્રણ ક્રિયાઓ"),
("Display Settings", "ડિસ્પ્લે સેટિંગ્સ"),
("Ratio", "રેશિયો (Ratio)"),
("Image Quality", "ઇમેજ ગુણવત્તા"),
("Scroll Style", "સ્ક્રોલ શૈલી"),
("Show Toolbar", "ટૂલબાર બતાવો"),
("Hide Toolbar", "ટૂલબાર છુપાવો"),
("Direct Connection", "સીધું કનેક્શન"),
("Relay Connection", "રિલે કનેક્શન"),
("Secure Connection", "સુરક્ષિત કનેક્શન"),
("Insecure Connection", "અસુરક્ષિત કનેક્શન"),
("Scale original", "મૂળ સ્કેલ"),
("Scale adaptive", "એડેપ્ટિવ સ્કેલ"),
("General", "સામાન્ય"),
("Security", "સુરક્ષા"),
("Theme", "થીમ"),
("Dark Theme", "ડાર્ક થીમ"),
("Light Theme", "લાઇટ થીમ"),
("Dark", "ડાર્ક"),
("Light", "લાઇટ"),
("Follow System", "સિસ્ટમ મુજબ"),
("Enable hardware codec", "હાર્ડવેર કોડેક સક્ષમ કરો"),
("Unlock Security Settings", "સુરક્ષા સેટિંગ્સ અનલોક કરો"),
("Enable audio", "ઓડિયો સક્ષમ કરો"),
("Unlock Network Settings", "નેટવર્ક સેટિંગ્સ અનલોક કરો"),
("Server", "સર્વર"),
("Direct IP Access", "સીધું IP એક્સેસ"),
("Proxy", "પ્રોક્સી"),
("Apply", "લાગુ કરો"),
("Disconnect all devices?", "તમામ ઉપકરણો ડિસ્કનેક્ટ કરવા છે?"),
("Clear", "સાફ કરો"),
("Audio Input Device", "ઓડિયો ઇનપુટ ઉપકરણ"),
("Use IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગનો ઉપયોગ કરો"),
("Network", "નેટવર્ક"),
("Pin Toolbar", "ટૂલબાર પિન કરો"),
("Unpin Toolbar", "ટૂલબાર અનપિન કરો"),
("Recording", "રેકોર્ડિંગ"),
("Directory", "ડિરેક્ટરી"),
("Automatically record incoming sessions", "આવતા સત્રો આપમેળે રેકોર્ડ કરો"),
("Automatically record outgoing sessions", "જતા સત્રો આપમેળે રેકોર્ડ કરો"),
("Change", "બદલો"),
("Start session recording", "સત્ર રેકોર્ડિંગ શરૂ કરો"),
("Stop session recording", "સત્ર રેકોર્ડિંગ બંધ કરો"),
("Enable recording session", "સત્ર રેકોર્ડિંગ સક્ષમ કરો"),
("Enable LAN discovery", "LAN ડિસ્કવરી સક્ષમ કરો"),
("Deny LAN discovery", "LAN ડિસ્કવરી નકારો"),
("Write a message", "સંદેશ લખો"),
("Prompt", "પ્રોમ્પ્ટ"),
("Please wait for confirmation of UAC...", "કૃપા કરીને UAC પુષ્ટિની રાહ જુઓ..."),
("elevated_foreground_window_tip", "રિમોટની વર્તમાન વિન્ડોને વધારે પરવાનગીની જરૂર છે."),
("Disconnected", "ડિસ્કનેક્ટ થઈ ગયું"),
("Other", "અન્ય"),
("Confirm before closing multiple tabs", "બહુવિધ ટેબ્સ બંધ કરતા પહેલા પુષ્ટિ કરો"),
("Keyboard Settings", "કીબોર્ડ સેટિંગ્સ"),
("Full Access", "પૂર્ણ એક્સેસ"),
("Screen Share", "સ્ક્રીન શેર"),
("ubuntu-21-04-required", "Ubuntu 21.04 કે તેથી ઉપર જરૂરી છે"),
("wayland-requires-higher-linux-version", "Wayland માટે ઉચ્ચ Linux વર્ઝન જરૂરી છે"),
("xdp-portal-unavailable", "XDP પોર્ટલ અનુપલબ્ધ છે"),
("JumpLink", "JumpLink"),
("Please Select the screen to be shared(Operate on the peer side).", "કૃપા કરીને શેર કરવાની સ્ક્રીન પસંદ કરો (સામેના છેડે કાર્ય કરો)."),
("Show RustDesk", "RustDesk બતાવો"),
("This PC", "આ PC"),
("or", "અથવા"),
("Elevate", "એલિવેટ કરો"),
("Zoom cursor", "ઝૂમ કર્સર"),
("Accept sessions via password", "પાસવર્ડ દ્વારા સત્રો સ્વીકારો"),
("Accept sessions via click", "ક્લિક દ્વારા સત્રો સ્વીકારો"),
("Accept sessions via both", "બંને દ્વારા સત્રો સ્વીકારો"),
("Please wait for the remote side to accept your session request...", "કૃપા કરીને સામેનો છેડો વિનંતી સ્વીકારે તેની રાહ જુઓ..."),
("One-time Password", "વન-ટાઇમ પાસવર્ડ (OTP)"),
("Use one-time password", "વન-ટાઇમ પાસવર્ડનો ઉપયોગ કરો"),
("One-time password length", "OTP ની લંબાઈ"),
("Request access to your device", "તમારા ઉપકરણના એક્સેસ માટે વિનંતી"),
("Hide connection management window", "કનેક્શન મેનેજમેન્ટ વિન્ડો છુપાવો"),
("hide_cm_tip", "જો પાસવર્ડ દ્વારા કનેક્શન હોય તો જ છુપાવો"),
("wayland_experiment_tip", "Wayland સપોર્ટ હજુ પ્રાયોગિક ધોરણે છે"),
("Right click to select tabs", "ટેબ્સ પસંદ કરવા રાઇટ ક્લિક કરો"),
("Skipped", "રહેવા દીધું (Skipped)"),
("Add to address book", "એડ્રેસ બુકમાં ઉમેરો"),
("Group", "ગ્રુપ"),
("Search", "શોધો"),
("Closed manually by web console", "વેબ કન્સોલ દ્વારા મેન્યુઅલી બંધ કરવામાં આવ્યું"),
("Local keyboard type", "લોકલ કીબોર્ડ પ્રકાર"),
("Select local keyboard type", "લોકલ કીબોર્ડ પ્રકાર પસંદ કરો"),
("software_render_tip", "જો સ્ક્રીન કાળી દેખાય, તો આ અજમાવો"),
("Always use software rendering", "હંમેશા સોફ્ટવેર રેન્ડરિંગનો ઉપયોગ કરો"),
("config_input", "ઇનપુટ કોન્ફિગર કરો"),
("config_microphone", "માઇક્રોફોન કોન્ફિગર કરો"),
("request_elevation_tip", "સામેથી ઉચ્ચ પરવાનગી (Elevation) માટે વિનંતી કરો"),
("Wait", "રાહ જુઓ"),
("Elevation Error", "એલિવેશન ભૂલ"),
("Ask the remote user for authentication", "સામેના યુઝરને ઓથેન્ટિકેશન માટે પૂછો"),
("Choose this if the remote account is administrator", "જો સામેનું ખાતું એડમિનિસ્ટ્રેટર હોય તો આ પસંદ કરો"),
("Transmit the username and password of administrator", "એડમિનિસ્ટ્રેટરનું નામ અને પાસવર્ડ મોકલો"),
("still_click_uac_tip", "રિમોટ યુઝરે હજુ પણ UAC વિન્ડોમાં 'હા' ક્લિક કરવું પડશે."),
("Request Elevation", "એલિવેશન માટે વિનંતી કરો"),
("wait_accept_uac_tip", "કૃપા કરીને સામેનો યુઝર UAC સ્વીકારે તેની રાહ જુઓ."),
("Elevate successfully", "સફળતાપૂર્વક એલિવેટ થયું"),
("uppercase", "મોટા અક્ષરો (Uppercase)"),
("lowercase", "નાના અક્ષરો (Lowercase)"),
("digit", "અંક (Digit)"),
("special character", "ખાસ અક્ષર"),
("length>=8", "લંબાઈ >= 8"),
("Weak", "નબળું"),
("Medium", "મધ્યમ"),
("Strong", "મજબૂત"),
("Switch Sides", "બાજુઓ બદલો"),
("Please confirm if you want to share your desktop?", "શું તમે તમારું ડેસ્કટોપ શેર કરવા માંગો છો?"),
("Display", "ડિસ્પ્લે"),
("Default View Style", "ડિફોલ્ટ વ્યુ શૈલી"),
("Default Scroll Style", "ડિફોલ્ટ સ્ક્રોલ શૈલી"),
("Default Image Quality", "ડિફોલ્ટ ઇમેજ ગુણવત્તા"),
("Default Codec", "ડિફોલ્ટ કોડેક"),
("Bitrate", "બિટરેટ"),
("FPS", "FPS"),
("Auto", "ઓટો"),
("Other Default Options", "અન્ય ડિફોલ્ટ વિકલ્પો"),
("Voice call", "વોઇસ કોલ"),
("Text chat", "ટેક્સ્ટ ચેટ"),
("Stop voice call", "વોઇસ કોલ બંધ કરો"),
("relay_hint_tip", "સીધું કનેક્શન શક્ય નથી; તમે રિલે દ્વારા પ્રયાસ કરી શકો છો."),
("Reconnect", "ફરી કનેક્ટ કરો"),
("Codec", "કોડેક"),
("Resolution", "રિઝોલ્યુશન"),
("No transfers in progress", "કોઈ ટ્રાન્સફર ચાલુ નથી"),
("Set one-time password length", "OTP લંબાઈ સેટ કરો"),
("RDP Settings", "RDP સેટિંગ્સ"),
("Sort by", "ક્રમબદ્ધ કરો"),
("New Connection", "નવું કનેક્શન"),
("Restore", "રીસ્ટોર"),
("Minimize", "મિનિમાઇઝ"),
("Maximize", "મેક્સિમાઇઝ"),
("Your Device", "તમારું ઉપકરણ"),
("empty_recent_tip", "તાજેતરના સત્રો અહીં દેખાશે."),
("empty_favorite_tip", "પસંદગીના ઉપકરણો અહીં દેખાશે."),
("empty_lan_tip", "નેટવર્ક પરના ઉપકરણો અહીં દેખાશે."),
("empty_address_book_tip", "તમારી એડ્રેસ બુક ખાલી છે."),
("Empty Username", "ખાલી યુઝરનેમ"),
("Empty Password", "ખાલી પાસવર્ડ"),
("Me", "હું"),
("identical_file_tip", "આ ફાઇલ પહેલેથી જ અસ્તિત્વમાં છે."),
("show_monitors_tip", "ટૂલબારમાં મોનિટર બતાવો"),
("View Mode", "વ્યુ મોડ"),
("login_linux_tip", "રિમોટ Linux સત્ર માટે તમારે લોગિન કરવું પડશે"),
("verify_rustdesk_password_tip", "RustDesk પાસવર્ડ ચકાસો"),
("remember_account_tip", "આ ખાતું યાદ રાખો"),
("os_account_desk_tip", "એક્સેસ માટે OS ખાતાનો ઉપયોગ કરો"),
("OS Account", "OS ખાતું"),
("another_user_login_title_tip", "બીજો યુઝર પહેલેથી લોગિન છે"),
("another_user_login_text_tip", "ડિસ્કનેક્ટ કરો અને ફરી પ્રયાસ કરો"),
("xorg_not_found_title_tip", "Xorg મળ્યું નથી"),
("xorg_not_found_text_tip", "કૃપા કરીને Xorg ઇન્સ્ટોલ કરો"),
("no_desktop_title_tip", "કોઈ ડેસ્કટોપ ઉપલબ્ધ નથી"),
("no_desktop_text_tip", "કૃપા કરીને Linux ડેસ્કટોપ ઇન્સ્ટોલ કરો"),
("No need to elevate", "એલિવેટ કરવાની જરૂર નથી"),
("System Sound", "સિસ્ટમ સાઉન્ડ"),
("Default", "ડિફોલ્ટ"),
("New RDP", "નવું RDP"),
("Fingerprint", "ફિંગરપ્રિન્ટ"),
("Copy Fingerprint", "ફિંગરપ્રિન્ટ કોપી કરો"),
("no fingerprints", "કોઈ ફિંગરપ્રિન્ટ નથી"),
("Select a peer", "એક પીઅર પસંદ કરો"),
("Select peers", "પીઅર્સ પસંદ કરો"),
("Plugins", "પ્લગઇન્સ"),
("Uninstall", "અનઇન્સ્ટોલ કરો"),
("Update", "અપડેટ કરો"),
("Enable", "સક્ષમ કરો"),
("Disable", "અક્ષમ કરો"),
("Options", "વિકલ્પો"),
("resolution_original_tip", "મૂળ રિઝોલ્યુશન"),
("resolution_fit_local_tip", "સ્ક્રીન મુજબ ફીટ કરો"),
("resolution_custom_tip", "કસ્ટમ રિઝોલ્યુશન"),
("Collapse toolbar", "ટૂલબાર નાનું કરો"),
("Accept and Elevate", "સ્વીકારો અને એલિવેટ કરો"),
("accept_and_elevate_btn_tooltip", "કનેક્શન સ્વીકારો અને UAC પરવાનગીઓ મેળવો."),
("clipboard_wait_response_timeout_tip", "ક્લિપબોર્ડ પ્રતિક્રિયા માટે સમય સમાપ્ત થયો."),
("Incoming connection", "આવતું કનેક્શન"),
("Outgoing connection", "જતું કનેક્શન"),
("Exit", "બહાર નીકળો"),
("Open", "ખોલો"),
("logout_tip", "શું તમે ખરેખર લોગઆઉટ કરવા માંગો છો?"),
("Service", "સેવા"),
("Start", "શરૂ કરો"),
("Stop", "બંધ કરો"),
("exceed_max_devices", "તમે ઉપકરણોની મહત્તમ મર્યાદા વટાવી દીધી છે."),
("Sync with recent sessions", "તાજેતરના સત્રો સાથે સિંક કરો"),
("Sort tags", "ટેગ્સ ક્રમબદ્ધ કરો"),
("Open connection in new tab", "નવી ટેબમાં કનેક્શન ખોલો"),
("Move tab to new window", "ટેબને નવી વિન્ડોમાં ખસેડો"),
("Can not be empty", "ખાલી ન હોઈ શકે"),
("Already exists", "પહેલેથી અસ્તિત્વમાં છે"),
("Change Password", "પાસવર્ડ બદલો"),
("Refresh Password", "પાસવર્ડ રિફ્રેશ કરો"),
("ID", "ID"),
("Grid View", "ગ્રીડ વ્યુ"),
("List View", "લિસ્ટ વ્યુ"),
("Select", "પસંદ કરો"),
("Toggle Tags", "ટેગ્સ ચાલુ/બંધ કરો"),
("pull_ab_failed_tip", "એડ્રેસ બુક અપડેટ કરવામાં નિષ્ફળ."),
("push_ab_failed_tip", "એડ્રેસ બુક સિંક કરવામાં નિષ્ફળ."),
("synced_peer_readded_tip", "તાજેતરના સત્રોના ઉપકરણો એડ્રેસ બુકમાં સિંક થયા."),
("Change Color", "રંગ બદલો"),
("Primary Color", "પ્રાથમિક રંગ"),
("HSV Color", "HSV રંગ"),
("Installation Successful!", "ઇન્સ્ટોલેશન સફળ!"),
("Installation failed!", "ઇન્સ્ટોલેશન નિષ્ફળ!"),
("Reverse mouse wheel", "માઉસ વ્હીલ ઊલટું કરો"),
("{} sessions", "{} સત્રો"),
("scam_title", "છેતરપિંડીની ચેતવણી!"),
("scam_text1", "જો તમે અજાણી વ્યક્તિ સાથે વાત કરી રહ્યા હો અને તેણે RustDesk વાપરવા કહ્યું હોય, તો તરત ડિસ્કનેક્ટ કરો."),
("scam_text2", "આ એક છેતરપિંડી હોઈ શકે છે. કોઈને પાસવર્ડ આપશો નહીં."),
("Don't show again", "ફરીથી ના બતાવશો"),
("I Agree", "હું સહમત છું"),
("Decline", "અસ્વીકાર"),
("Timeout in minutes", "મિનિટોમાં ટાઇમઆઉટ"),
("auto_disconnect_option_tip", "નિષ્ક્રિયતા પર આપમેળે ડિસ્કનેક્ટ કરો"),
("Connection failed due to inactivity", "નિષ્ક્રિયતાને કારણે કનેક્શન નિષ્ફળ"),
("Check for software update on startup", "શરૂઆતમાં અપડેટ તપાસો"),
("upgrade_rustdesk_server_pro_to_{}_tip", "સર્વર પ્રો ને {} માં અપગ્રેડ કરો"),
("pull_group_failed_tip", "ગ્રુપ ખેંચવામાં (Pull) નિષ્ફળ"),
("Filter by intersection", "ઇન્ટરસેક્શન દ્વારા ફિલ્ટર કરો"),
("Remove wallpaper during incoming sessions", "કનેક્શન દરમિયાન વોલપેપર હટાવો"),
("Test", "ટેસ્ટ"),
("display_is_plugged_out_msg", "ડિસ્પ્લે કાઢી નાખવામાં આવ્યું છે."),
("No displays", "કોઈ ડિસ્પ્લે નથી"),
("Open in new window", "નવી વિન્ડોમાં ખોલો"),
("Show displays as individual windows", "દરેક ડિસ્પ્લે અલગ વિન્ડોમાં બતાવો"),
("Use all my displays for the remote session", "તમામ ડિસ્પ્લેનો ઉપયોગ કરો"),
("selinux_tip", "SELinux ઉપકરણ પર સક્ષમ છે."),
("Change view", "વ્યુ બદલો"),
("Big tiles", "મોટી ટાઇલ્સ"),
("Small tiles", "નાની ટાઇલ્સ"),
("List", "લિસ્ટ"),
("Virtual display", "વર્ચ્યુઅલ ડિસ્પ્લે"),
("Plug out all", "બધું કાઢી નાખો (Plug out)"),
("True color (4:4:4)", "ટ્રુ કલર (4:4:4)"),
("Enable blocking user input", "યુઝર ઇનપુટ બ્લોકિંગ સક્ષમ કરો"),
("id_input_tip", "તમે ID, Alias અથવા IP એડ્રેસ દાખલ કરી શકો છો."),
("privacy_mode_impl_mag_tip", "મેગ્નિફાયર પ્રાઇવસી મોડ"),
("privacy_mode_impl_virtual_display_tip", "વર્ચ્યુઅલ ડિસ્પ્લે પ્રાઇવસી મોડ"),
("Enter privacy mode", "પ્રાઇવસી મોડમાં પ્રવેશ કરો"),
("Exit privacy mode", "પ્રાઇવસી મોડમાંથી બહાર નીકળો"),
("idd_not_support_under_win10_2004_tip", "વર્ચ્યુઅલ ડિસ્પ્લે Windows 10 (2004) કે તેથી ઉપર જ સક્ષમ છે."),
("input_source_1_tip", "ઇનપુટ સ્ત્રોત ૧"),
("input_source_2_tip", "ઇનપુટ સ્ત્રોત ૨"),
("Swap control-command key", "Control અને Command કી બદલો"),
("swap-left-right-mouse", "ડાબું અને જમણું માઉસ બટન બદલો"),
("2FA code", "2FA કોડ"),
("More", "વધારે"),
("enable-2fa-title", "2FA સક્ષમ કરો"),
("enable-2fa-desc", "તમારું ઓથેન્ટિકેટર એપ સેટ કરો."),
("wrong-2fa-code", "ખોટો 2FA કોડ."),
("enter-2fa-title", "2FA કોડ દાખલ કરો"),
("Email verification code must be 6 characters.", "ઇમેઇલ કોડ 6 અક્ષરનો હોવો જોઈએ."),
("2FA code must be 6 digits.", "2FA કોડ 6 અંકનો હોવો જોઈએ."),
("Multiple Windows sessions found", "બહુવિધ Windows સત્રો મળ્યા"),
("Please select the session you want to connect to", "કૃપા કરીને જે સત્ર સાથે જોડાવું હોય તે પસંદ કરો"),
("powered_by_me", "મારા દ્વારા સંચાલિત"),
("outgoing_only_desk_tip", "આ માત્ર આઉટગોઇંગ મોડ છે"),
("preset_password_warning", "સુરક્ષા માટે પાસવર્ડ બદલો."),
("Security Alert", "સુરક્ષા ચેતવણી"),
("My address book", "મારી એડ્રેસ બુક"),
("Personal", "વ્યક્તિગત"),
("Owner", "માલિક"),
("Set shared password", "શેર કરેલ પાસવર્ડ સેટ કરો"),
("Exist in", "માં અસ્તિત્વ ધરાવે છે"),
("Read-only", "માત્ર વાંચવા માટે"),
("Read/Write", "વાંચવા/લખવા માટે"),
("Full Control", "પૂર્ણ નિયંત્રણ"),
("share_warning_tip", "તમે તમારો એક્સેસ શેર કરી રહ્યા છો."),
("Everyone", "દરેક વ્યક્તિ"),
("ab_web_console_tip", "વેબ કન્સોલ એડ્રેસ બુક"),
("allow-only-conn-window-open-tip", "માત્ર RustDesk વિન્ડો ખુલ્લી હોય ત્યારે જ કનેક્શનની મંજૂરી આપો"),
("no_need_privacy_mode_no_physical_displays_tip", "ભૌતિક ડિસ્પ્લે નથી, પ્રાઇવસી મોડની જરૂર નથી."),
("Follow remote cursor", "રિમોટ કર્સરને અનુસરો"),
("Follow remote window focus", "રિમોટ વિન્ડો ફોકસને અનુસરો"),
("default_proxy_tip", "ડિફોલ્ટ પ્રોક્સી સેટિંગ"),
("no_audio_input_device_tip", "કોઈ ઓડિયો ઇનપુટ મળ્યું નથી."),
("Incoming", "આવતું"),
("Outgoing", "જતું"),
("Clear Wayland screen selection", "Wayland સ્ક્રીન સિલેક્શન સાફ કરો"),
("clear_Wayland_screen_selection_tip", "સ્ક્રીન સિલેક્શન રીસેટ કરો."),
("confirm_clear_Wayland_screen_selection_tip", "શું તમે સિલેક્શન સાફ કરવા માંગો છો?"),
("android_new_voice_call_tip", "નવો વોઇસ કોલ વિનંતી"),
("texture_render_tip", "ટેક્સચર રેન્ડરિંગ વાપરો"),
("Use texture rendering", "ટેક્સચર રેન્ડરિંગનો ઉપયોગ કરો"),
("Floating window", "ફ્લોટિંગ વિન્ડો"),
("floating_window_tip", "બેકગ્રાઉન્ડમાં હોય ત્યારે RustDesk બતાવો"),
("Keep screen on", "સ્ક્રીન ચાલુ રાખો"),
("Never", "ક્યારેય નહીં"),
("During controlled", "નિયંત્રણ દરમિયાન"),
("During service is on", "જ્યારે સેવા ચાલુ હોય ત્યારે"),
("Capture screen using DirectX", "DirectX દ્વારા સ્ક્રીન કેપ્ચર કરો"),
("Back", "પાછળ"),
("Apps", "એપ્સ"),
("Volume up", "અવાજ વધારો"),
("Volume down", "અવાજ ઘટાડો"),
("Power", "પાવર"),
("Telegram bot", "Telegram બોટ"),
("enable-bot-tip", "સૂચનાઓ માટે બોટ સક્ષમ કરો"),
("enable-bot-desc", "સૂચનાઓ માટે ટેલિગ્રામ બોટ સેટ કરો."),
("cancel-2fa-confirm-tip", "શું તમે 2FA રદ કરવા માંગો છો?"),
("cancel-bot-confirm-tip", "શું તમે બોટ રદ કરવા માંગો છો?"),
("About RustDesk", "RustDesk વિશે"),
("Send clipboard keystrokes", "ક્લિપબોર્ડ કી-સ્ટ્રોક્સ મોકલો"),
("network_error_tip", "નેટવર્ક ભૂલ, ફરી પ્રયાસ કરો."),
("Unlock with PIN", "PIN થી અનલોક કરો"),
("Requires at least {} characters", "ઓછામાં ઓછા {} અક્ષર જરૂરી"),
("Wrong PIN", "ખોટો PIN"),
("Set PIN", "PIN સેટ કરો"),
("Enable trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સક્ષમ કરો"),
("Manage trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સંચાલિત કરો"),
("Platform", "પ્લેટફોર્મ"),
("Days remaining", "બાકી દિવસો"),
("enable-trusted-devices-tip", "માત્ર વિશ્વાસપાત્ર ઉપકરણો જ પાસવર્ડ વગર જોડાઈ શકે"),
("Parent directory", "પેરન્ટ ડિરેક્ટરી"),
("Resume", "ફરી શરૂ કરો"),
("Invalid file name", "અમાન્ય ફાઇલ નામ"),
("one-way-file-transfer-tip", "માત્ર એકતરફી ફાઇલ ટ્રાન્સફરની મંજૂરી છે"),
("Authentication Required", "ઓથેન્ટિકેશન જરૂરી"),
("Authenticate", "ઓથેન્ટિકેટ કરો"),
("web_id_input_tip", "રિમોટ ID દાખલ કરો"),
("Download", "ડાઉનલોડ"),
("Upload folder", "ફોલ્ડર અપલોડ કરો"),
("Upload files", "ફાઇલો અપલોડ કરો"),
("Clipboard is synchronized", "ક્લિપબોર્ડ સિંક થયેલ છે"),
("Update client clipboard", "ક્લાયન્ટ ક્લિપબોર્ડ અપડેટ કરો"),
("Untagged", "ટેગ વગરનું"),
("new-version-of-{}-tip", "{} નું નવું વર્ઝન ઉપલબ્ધ છે"),
("Accessible devices", "એક્સેસિબલ ઉપકરણો"),
("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"),
("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"),
("Printer", "પ્રિન્ટર"),
("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."),
("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."),
("printer-{}-not-installed-tip", "પ્રિન્ટર {} ઇન્સ્ટોલ નથી."),
("printer-{}-ready-tip", "પ્રિન્ટર {} તૈયાર છે."),
("Install {} Printer", "{} પ્રિન્ટર ઇન્સ્ટોલ કરો"),
("Outgoing Print Jobs", "જતા પ્રિન્ટ કાર્યો"),
("Incoming Print Jobs", "આવતા પ્રિન્ટ કાર્યો"),
("Incoming Print Job", "આવતું પ્રિન્ટ કાર્ય"),
("use-the-default-printer-tip", "ડિફોલ્ટ પ્રિન્ટર વાપરો"),
("use-the-selected-printer-tip", "પસંદ કરેલ પ્રિન્ટર વાપરો"),
("auto-print-tip", "આપમેળે પ્રિન્ટ કરો"),
("print-incoming-job-confirm-tip", "પ્રિન્ટ કરતા પહેલા પુષ્ટિ કરો"),
("remote-printing-disallowed-tile-tip", "રિમોટ પ્રિન્ટિંગની મંજૂરી નથી"),
("remote-printing-disallowed-text-tip", "સેટિંગ્સમાં રિમોટ પ્રિન્ટિંગ સક્ષમ કરો."),
("save-settings-tip", "સેટિંગ્સ સાચવો"),
("dont-show-again-tip", "ફરીથી ના બતાવશો"),
("Take screenshot", "સ્ક્રીનશોટ લો"),
("Taking screenshot", "સ્ક્રીનશોટ લેવાઈ રહ્યો છે"),
("screenshot-merged-screen-not-supported-tip", "મર્જ કરેલ સ્ક્રીનશોટ સપોર્ટેડ નથી."),
("screenshot-action-tip", "સ્ક્રીનશોટ પછીની ક્રિયા"),
("Save as", "તરીકે સાચવો"),
("Copy to clipboard", "ક્લિપબોર્ડમાં કોપી કરો"),
("Enable remote printer", "રિમોટ પ્રિન્ટર સક્ષમ કરો"),
("Downloading {}", "{} ડાઉનલોડ થઈ રહ્યું છે"),
("{} Update", "{} અપડેટ"),
("{}-to-update-tip", "અપડેટ કરવા માટે {}"),
("download-new-version-failed-tip", "નવું વર્ઝન ડાઉનલોડ કરવામાં નિષ્ફળ."),
("Auto update", "ઓટો અપડેટ"),
("update-failed-check-msi-tip", "અપડેટ નિષ્ફળ, MSI ફાઇલ તપાસો."),
("websocket_tip", "જો પોર્ટ બ્લોક હોય તો WebSocket વાપરો."),
("Use WebSocket", "WebSocket નો ઉપયોગ કરો"),
("Trackpad speed", "ટ્રેકપેડ સ્પીડ"),
("Default trackpad speed", "ડિફોલ્ટ ટ્રેકપેડ સ્પીડ"),
("Numeric one-time password", "ન્યુમેરિક OTP"),
("Enable IPv6 P2P connection", "IPv6 P2P કનેક્શન સક્ષમ કરો"),
("Enable UDP hole punching", "UDP હોલ પંચિંગ સક્ષમ કરો"),
("View camera", "કેમેરા જુઓ"),
("Enable camera", "કેમેરા સક્ષમ કરો"),
("No cameras", "કોઈ કેમેરા મળ્યો નથી"),
("view_camera_unsupported_tip", "રિમોટ કેમેરા સપોર્ટેડ નથી."),
("Terminal", "ટર્મિનલ"),
("Enable terminal", "ટર્મિનલ સક્ષમ કરો"),
("New tab", "નવી ટેબ"),
("Keep terminal sessions on disconnect", "ડિસ્કનેક્ટ વખતે ટર્મિનલ ચાલુ રાખો"),
("Terminal (Run as administrator)", "ટર્મિનલ (એડમિનિસ્ટ્રેટર તરીકે)"),
("terminal-admin-login-tip", "એડમિન લોગિન જરૂરી છે."),
("Failed to get user token.", "યુઝર ટોકન મેળવવામાં નિષ્ફળ."),
("Incorrect username or password.", "ખોટું યુઝરનેમ કે પાસવર્ડ."),
("The user is not an administrator.", "યુઝર એડમિનિસ્ટ્રેટર નથી."),
("Failed to check if the user is an administrator.", "યુઝર એડમિન છે કે નહીં તે ચકાસવામાં નિષ્ફળ."),
("Supported only in the installed version.", "માત્ર ઇન્સ્ટોલ કરેલ વર્ઝનમાં ઉપલબ્ધ."),
("elevation_username_tip", "એડમિનિસ્ટ્રેટર નામ દાખલ કરો"),
("Preparing for installation ...", "ઇન્સ્ટોલેશનની તૈયારી..."),
("Show my cursor", "મારું કર્સર બતાવો"),
("Scale custom", "કસ્ટમ સ્કેલ"),
("Custom scale slider", "કસ્ટમ સ્કેલ સ્લાઇડર"),
("Decrease", "ઘટાડો"),
("Increase", "વધારો"),
("Show virtual mouse", "વર્ચ્યુઅલ માઉસ બતાવો"),
("Virtual mouse size", "વર્ચ્યુઅલ માઉસ કદ"),
("Small", "નાનું"),
("Large", "મોટું"),
("Show virtual joystick", "વર્ચ્યુઅલ જોયસ્ટિક બતાવો"),
("Edit note", "નોંધ સુધારો"),
("Alias", "Alias (ઉપનામ)"),
("ScrollEdge", "સ્ક્રોલ એજ"),
("Allow insecure TLS fallback", "અસુરક્ષિત TLS ફોલબેકની મંજૂરી આપો"),
("allow-insecure-tls-fallback-tip", "જૂના સર્વર માટે વાપરો."),
("Disable UDP", "UDP અક્ષમ કરો"),
("disable-udp-tip", "કનેક્શન સમસ્યાઓ માટે UDP બંધ કરો."),
("server-oss-not-support-tip", "OSS સર્વર આને સપોર્ટ કરતું નથી."),
("input note here", "અહીં નોંધ લખો"),
("note-at-conn-end-tip", "કનેક્શનના અંતે નોંધ બતાવો"),
("Show terminal extra keys", "ટર્મિનલની વધારાની કી બતાવો"),
("Relative mouse mode", "રીલેટિવ માઉસ મોડ"),
("rel-mouse-not-supported-peer-tip", "સામેથી સપોર્ટેડ નથી."),
("rel-mouse-not-ready-tip", "તૈયાર નથી."),
("rel-mouse-lock-failed-tip", "માઉસ લોક નિષ્ફળ."),
("rel-mouse-exit-{}-tip", "બહાર નીકળવા {} દબાવો"),
("rel-mouse-permission-lost-tip", "પરવાનગી ગુમાવી દીધી."),
("Changelog", "Changelog (ફેરફારો)"),
("keep-awake-during-outgoing-sessions-label", "આઉટગોઇંગ સત્ર વખતે જાગૃત રાખો"),
("keep-awake-during-incoming-sessions-label", "ઇનકમિંગ સત્ર વખતે જાગૃત રાખો"),
("Continue with {}", "{} સાથે આગળ વધો"),
("Display Name", "ડિસ્પ્લે નામ"),
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
].iter().cloned().collect();
}

746
src/lang/hi.rs Normal file
View File

@@ -0,0 +1,746 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "स्थिति"),
("Your Desktop", "आपका डेस्कटॉप"),
("desk_tip", "आपका डेस्कटॉप इस आईडी और पासवर्ड से एक्सेस किया जा सकता है।"),
("Password", "पासवर्ड"),
("Ready", "तैयार"),
("Established", "स्थापित"),
("connecting_status", "नेटवर्क से जुड़ रहा है..."),
("Enable service", "सेवा सक्षम करें"),
("Start service", "सेवा शुरू करें"),
("Service is running", "सेवा चल रही है"),
("Service is not running", "सेवा नहीं चल रही है"),
("not_ready_status", "तैयार नहीं। कृपया अपना कनेक्शन जांचें"),
("Control Remote Desktop", "रिमोट डेस्कटॉप नियंत्रित करें"),
("Transfer file", "फ़ाइल स्थानांतरण"),
("Connect", "जुड़ें"),
("Recent sessions", "हाल के सत्र"),
("Address book", "पता पुस्तिका"),
("Confirmation", "पुष्टि"),
("TCP tunneling", "TCP टनलिंग"),
("Remove", "हटाएं"),
("Refresh random password", "यादृच्छिक (Random) पासवर्ड बदलें"),
("Set your own password", "अपना पासवर्ड सेट करें"),
("Enable keyboard/mouse", "कीबोर्ड/माउस सक्षम करें"),
("Enable clipboard", "क्लिपबोर्ड सक्षम करें"),
("Enable file transfer", "फ़ाइल स्थानांतरण सक्षम करें"),
("Enable TCP tunneling", "TCP टनलिंग सक्षम करें"),
("IP Whitelisting", "IP श्वेतसूची (Whitelisting)"),
("ID/Relay Server", "ID/रिले सर्वर"),
("Import server config", "सर्वर कॉन्फ़िगरेशन इम्पोर्ट करें"),
("Export Server Config", "सर्वर कॉन्फ़िगरेशन एक्सपोर्ट करें"),
("Import server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक इम्पोर्ट किया गया"),
("Export server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक एक्सपोर्ट किया गया"),
("Invalid server configuration", "अमान्य सर्वर कॉन्फ़िगरेशन"),
("Clipboard is empty", "क्लिपबोर्ड खाली है"),
("Stop service", "सेवा रोकें"),
("Change ID", "ID बदलें"),
("Your new ID", "आपकी नई ID"),
("length %min% to %max%", "लंबाई %min% से %max% तक"),
("starts with a letter", "एक अक्षर से शुरू होता है"),
("allowed characters", "अनुमत अक्षर"),
("id_change_tip", "ID बदलने के बाद वर्तमान कनेक्शन टूट जाएगा।"),
("Website", "वेबसाइट"),
("About", "के बारे में"),
("Slogan_tip", "बेहतर अनुभव के लिए बनाया गया रिमोट डेस्कटॉप सॉफ़्टवेयर"),
("Privacy Statement", "गोपनीयता कथन"),
("Mute", "म्यूट करें"),
("Build Date", "निर्माण तिथि"),
("Version", "संस्करण"),
("Home", "होम"),
("Audio Input", "ऑडियो इनपुट"),
("Enhancements", "वृद्धि (Enhancements)"),
("Hardware Codec", "हार्डवेयर कोडेक"),
("Adaptive bitrate", "अनुकूली (Adaptive) बिटरेट"),
("ID Server", "ID सर्वर"),
("Relay Server", "रिले सर्वर"),
("API Server", "API सर्वर"),
("invalid_http", "अमान्य HTTP लिंक"),
("Invalid IP", "अमान्य IP"),
("Invalid format", "अमान्य प्रारूप"),
("server_not_support", "सर्वर द्वारा समर्थित नहीं"),
("Not available", "उपलब्ध नहीं"),
("Too frequent", "बहुत बार-बार"),
("Cancel", "रद्द करें"),
("Skip", "छोड़ें"),
("Close", "बंद करें"),
("Retry", "पुनः प्रयास करें"),
("OK", "ठीक है"),
("Password Required", "पासवर्ड आवश्यक है"),
("Please enter your password", "कृपया अपना पासवर्ड दर्ज करें"),
("Remember password", "पासवर्ड याद रखें"),
("Wrong Password", "गलत पासवर्ड"),
("Do you want to enter again?", "क्या आप दोबारा दर्ज करना चाहते हैं?"),
("Connection Error", "कनेक्शन त्रुटि"),
("Error", "त्रुटि"),
("Reset by the peer", "दूसरे सिस्टम द्वारा रिसेट किया गया"),
("Connecting...", "जुड़ रहा है..."),
("Connection in progress. Please wait.", "कनेक्शन जारी है। कृपया प्रतीक्षा करें।"),
("Please try 1 minute later", "कृपया 1 मिनट बाद पुनः प्रयास करें"),
("Login Error", "लॉगिन त्रुटि"),
("Successful", "सफल"),
("Connected, waiting for image...", "जुड़ गया, इमेज की प्रतीक्षा कर रहा है..."),
("Name", "नाम"),
("Type", "प्रकार"),
("Modified", "संशोधित"),
("Size", "आकार"),
("Show Hidden Files", "छिपी हुई फाइलें दिखाएं"),
("Receive", "प्राप्त करें"),
("Send", "भेजें"),
("Refresh File", "फ़ाइल रिफ्रेश करें"),
("Local", "स्थानीय (Local)"),
("Remote", "रिमोट"),
("Remote Computer", "रिमोट कंप्यूटर"),
("Local Computer", "स्थानीय कंप्यूटर"),
("Confirm Delete", "हटाने की पुष्टि करें"),
("Delete", "हटाएं"),
("Properties", "गुण (Properties)"),
("Multi Select", "बहु-चयन"),
("Select All", "सभी चुनें"),
("Unselect All", "सभी अचयनित करें"),
("Empty Directory", "खाली निर्देशिका"),
("Not an empty directory", "निर्देशिका खाली नहीं है"),
("Are you sure you want to delete this file?", "क्या आप वाकई इस फ़ाइल को हटाना चाहते हैं?"),
("Are you sure you want to delete this empty directory?", "क्या आप वाकई इस खाली निर्देशिका को हटाना चाहते हैं?"),
("Are you sure you want to delete the file of this directory?", "क्या आप वाकई इस निर्देशिका की फ़ाइल को हटाना चाहते हैं?"),
("Do this for all conflicts", "सभी विवादों के लिए यह करें"),
("This is irreversible!", "इसे वापस नहीं लिया जा सकता!"),
("Deleting", "हटाया जा रहा है"),
("files", "फाइलें"),
("Waiting", "प्रतीक्षा कर रहा है"),
("Finished", "पूरा हुआ"),
("Speed", "गति"),
("Custom Image Quality", "कस्टम इमेज गुणवत्ता"),
("Privacy mode", "गोपनीयता मोड"),
("Block user input", "उपयोगकर्ता इनपुट ब्लॉक करें"),
("Unblock user input", "उपयोगकर्ता इनपुट अनब्लॉक करें"),
("Adjust Window", "विंडो समायोजित करें"),
("Original", "मूल (Original)"),
("Shrink", "सिकुड़ें"),
("Stretch", "खिंचाव (Stretch)"),
("Scrollbar", "स्क्रोलबार"),
("ScrollAuto", "ऑटो स्क्रॉल"),
("Good image quality", "अच्छी इमेज गुणवत्ता"),
("Balanced", "संतुलित"),
("Optimize reaction time", "प्रतिक्रिया समय अनुकूलित करें"),
("Custom", "कस्टम"),
("Show remote cursor", "रिमोट कर्सर दिखाएं"),
("Show quality monitor", "गुणवत्ता मॉनिटर दिखाएं"),
("Disable clipboard", "क्लिपबोर्ड अक्षम करें"),
("Lock after session end", "सत्र समाप्त होने के बाद लॉक करें"),
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del डालें"),
("Insert Lock", "लॉक डालें"),
("Refresh", "रिफ्रेश करें"),
("ID does not exist", "ID मौजूद नहीं है"),
("Failed to connect to rendezvous server", "Rendezvous सर्वर से जुड़ने में विफल"),
("Please try later", "कृपया बाद में प्रयास करें"),
("Remote desktop is offline", "रिमोट डेस्कटॉप ऑफ़लाइन है"),
("Key mismatch", "कुंजी बेमेल (Key mismatch)"),
("Timeout", "समय समाप्त"),
("Failed to connect to relay server", "रिले सर्वर से जुड़ने में विफल"),
("Failed to connect via rendezvous server", "Rendezvous सर्वर के माध्यम से जुड़ने में विफल"),
("Failed to connect via relay server", "रिले सर्वर के माध्यम से जुड़ने में विफल"),
("Failed to make direct connection to remote desktop", "रिमोट डेस्कटॉप से सीधा कनेक्शन बनाने में विफल"),
("Set Password", "पासवर्ड सेट करें"),
("OS Password", "OS पासवर्ड"),
("install_tip", "सर्वोत्तम प्रदर्शन के लिए, इसे इंस्टॉल करें।"),
("Click to upgrade", "अपग्रेड करने के लिए क्लिक करें"),
("Configure", "कॉन्फ़िगर करें"),
("config_acc", "एक्सेसिबिलिटी कॉन्फ़िगर करें"),
("config_screen", "स्क्रीन कॉन्फ़िगर करें"),
("Installing ...", "इंस्टॉल हो रहा है..."),
("Install", "इंस्टॉल करें"),
("Installation", "इंस्टॉलेशन"),
("Installation Path", "इंस्टॉलेशन पाथ"),
("Create start menu shortcuts", "स्टार्ट मेनू शॉर्टकट बनाएं"),
("Create desktop icon", "डेस्कटॉप आइकन बनाएं"),
("agreement_tip", "इंस्टॉल करके आप लाइसेंस समझौते को स्वीकार करते हैं।"),
("Accept and Install", "स्वीकार करें और इंस्टॉल करें"),
("End-user license agreement", "अंतिम उपयोगकर्ता लाइसेंस समझौता"),
("Generating ...", "बनाया जा रहा है..."),
("Your installation is lower version.", "आपका वर्तमान इंस्टॉलेशन पुराना संस्करण है।"),
("not_close_tcp_tip", "टनल का उपयोग करते समय इस विंडो को बंद न करें।"),
("Listening ...", "सुन रहा है (Listening)..."),
("Remote Host", "रिमोट होस्ट"),
("Remote Port", "रिमोट पोर्ट"),
("Action", "कार्य"),
("Add", "जोड़ें"),
("Local Port", "स्थानीय पोर्ट"),
("Local Address", "स्थानीय पता"),
("Change Local Port", "स्थानीय पोर्ट बदलें"),
("setup_server_tip", "तेज़ कनेक्शन के लिए अपना खुद का सर्वर सेटअप करें"),
("Too short, at least 6 characters.", "बहुत छोटा, कम से कम 6 अक्षर होने चाहिए।"),
("The confirmation is not identical.", "पुष्टि समान नहीं है।"),
("Permissions", "अनुमतियाँ"),
("Accept", "स्वीकार करें"),
("Dismiss", "खारिज करें"),
("Disconnect", "डिस्कनेक्ट करें"),
("Enable file copy and paste", "फ़ाइल कॉपी और पेस्ट सक्षम करें"),
("Connected", "जुड़ गया"),
("Direct and encrypted connection", "सीधा और एन्क्रिप्टेड कनेक्शन"),
("Relayed and encrypted connection", "रिले और एन्क्रिप्टेड कनेक्शन"),
("Direct and unencrypted connection", "सीधा और अनएन्क्रिप्टेड कनेक्शन"),
("Relayed and unencrypted connection", "रिले और अनएन्क्रिप्टेड कनेक्शन"),
("Enter Remote ID", "रिमोट ID दर्ज करें"),
("Enter your password", "अपना पासवर्ड दर्ज करें"),
("Logging in...", "लॉग इन हो रहा है..."),
("Enable RDP session sharing", "RDP सत्र साझाकरण सक्षम करें"),
("Auto Login", "ऑटो लॉगिन"),
("Enable direct IP access", "सीधी IP पहुंच सक्षम करें"),
("Rename", "नाम बदलें"),
("Space", "स्थान (Space)"),
("Create desktop shortcut", "डेस्कटॉप शॉर्टकट बनाएं"),
("Change Path", "पाथ बदलें"),
("Create Folder", "फ़ोल्डर बनाएं"),
("Please enter the folder name", "कृपया फ़ोल्डर का नाम दर्ज करें"),
("Fix it", "इसे ठीक करें"),
("Warning", "चेतावनी"),
("Login screen using Wayland is not supported", "Wayland का उपयोग करने वाली लॉगिन स्क्रीन समर्थित नहीं है"),
("Reboot required", "रीबूट आवश्यक है"),
("Unsupported display server", "असमर्थित डिस्प्ले सर्वर"),
("x11 expected", "x11 अपेक्षित है"),
("Port", "पोर्ट"),
("Settings", "सेटिंग्स"),
("Username", "उपयोगकर्ता नाम"),
("Invalid port", "अमान्य पोर्ट"),
("Closed manually by the peer", "दूसरे सिस्टम द्वारा मैन्युअल रूप से बंद किया गया"),
("Enable remote configuration modification", "रिमोट कॉन्फ़िगरेशन संशोधन सक्षम करें"),
("Run without install", "बिना इंस्टॉल किए चलाएं"),
("Connect via relay", "रिले के माध्यम से जुड़ें"),
("Always connect via relay", "हमेशा रिले के माध्यम से जुड़ें"),
("whitelist_tip", "केवल श्वेतसूचीबद्ध IP ही मुझ तक पहुंच सकते हैं"),
("Login", "लॉगिन"),
("Verify", "सत्यापित करें"),
("Remember me", "मुझे याद रखें"),
("Trust this device", "इस डिवाइस पर भरोसा करें"),
("Verification code", "सत्यापन कोड"),
("verification_tip", "एक सत्यापन कोड आपके ईमेल पर भेजा गया है"),
("Logout", "लॉगआउट"),
("Tags", "टैग"),
("Search ID", "ID खोजें"),
("whitelist_sep", "अल्पविराम, अर्धविराम या रिक्त स्थान द्वारा अलग किया गया"),
("Add ID", "ID जोड़ें"),
("Add Tag", "टैग जोड़ें"),
("Unselect all tags", "सभी टैग अचयनित करें"),
("Network error", "नेटवर्क त्रुटि"),
("Username missed", "उपयोगकर्ता नाम छूट गया"),
("Password missed", "पासवर्ड छूट गया"),
("Wrong credentials", "गलत क्रेडेंशियल"),
("The verification code is incorrect or has expired", "सत्यापन कोड गलत है या समाप्त हो गया है"),
("Edit Tag", "टैग संपादित करें"),
("Forget Password", "पासवर्ड भूल गए"),
("Favorites", "पसंदीदा"),
("Add to Favorites", "पसंदीदा में जोड़ें"),
("Remove from Favorites", "पसंदीदा से हटाएं"),
("Empty", "खाली"),
("Invalid folder name", "अमान्य फ़ोल्डर नाम"),
("Socks5 Proxy", "Socks5 प्रॉक्सी"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) प्रॉक्सी"),
("Discovered", "खोजा गया"),
("install_daemon_tip", "बूट पर शुरू करने के लिए सेवा इंस्टॉल करें"),
("Remote ID", "रिमोट ID"),
("Paste", "पेस्ट करें"),
("Paste here?", "यहाँ पेस्ट करें?"),
("Are you sure to close the connection?", "क्या आप वाकई कनेक्शन बंद करना चाहते हैं?"),
("Download new version", "नया संस्करण डाउनलोड करें"),
("Touch mode", "टच मोड"),
("Mouse mode", "माउस मोड"),
("One-Finger Tap", "एक उंगली से टैप"),
("Left Mouse", "बायां माउस"),
("One-Long Tap", "एक लंबा टैप"),
("Two-Finger Tap", "दो उंगलियों से टैप"),
("Right Mouse", "दायां माउस"),
("One-Finger Move", "एक उंगली से हिलाएं"),
("Double Tap & Move", "डबल टैप और हिलाएं"),
("Mouse Drag", "माउस ड्रैग"),
("Three-Finger vertically", "तीन उंगलियां लंबवत"),
("Mouse Wheel", "माउस व्हील"),
("Two-Finger Move", "दो उंगलियों से हिलाएं"),
("Canvas Move", "कैनवास मूव"),
("Pinch to Zoom", "ज़ूम करने के लिए पिंच करें"),
("Canvas Zoom", "कैनवास ज़ूम"),
("Reset canvas", "कैनवास रिसेट करें"),
("No permission of file transfer", "फ़ाइल स्थानांतरण की अनुमति नहीं है"),
("Note", "नोट"),
("Connection", "कनेक्शन"),
("Share screen", "स्क्रीन शेयर करें"),
("Chat", "चैट"),
("Total", "कुल"),
("items", "आइटम"),
("Selected", "चयनित"),
("Screen Capture", "स्क्रीन कैप्चर"),
("Input Control", "इनपुट नियंत्रण"),
("Audio Capture", "ऑडियो कैप्चर"),
("Do you accept?", "क्या आप स्वीकार करते हैं?"),
("Open System Setting", "सिस्टम सेटिंग खोलें"),
("How to get Android input permission?", "Android इनपुट अनुमति कैसे प्राप्त करें?"),
("android_input_permission_tip1", "इनपुट अनुमति प्राप्त करने के लिए एक्सेसिबिलिटी सेवा सक्षम करें।"),
("android_input_permission_tip2", "कृपया सिस्टम सेटिंग में RustDesk खोजें और इसे चालू करें।"),
("android_new_connection_tip", "एक नया नियंत्रण अनुरोध प्राप्त हुआ है।"),
("android_service_will_start_tip", "स्क्रीन कैप्चर चालू करने से सेवा अपने आप शुरू हो जाएगी।"),
("android_stop_service_tip", "सेवा बंद करने से सभी कनेक्शन टूट जाएंगे।"),
("android_version_audio_tip", "ऑडियो कैप्चर केवल Android 10 या उच्चतर पर समर्थित है।"),
("android_start_service_tip", "स्क्रीन शेयरिंग सेवा शुरू करने के लिए क्लिक करें।"),
("android_permission_may_not_change_tip", "अनुमतियाँ बाद में नहीं बदली जा सकती हैं, कृपया ध्यान से चुनें।"),
("Account", "खाता"),
("Overwrite", "ओवरराइट (Overwrite) करें"),
("This file exists, skip or overwrite this file?", "यह फ़ाइल मौजूद है, छोड़ें या ओवरराइट करें?"),
("Quit", "बाहर निकलें"),
("Help", "सहायता"),
("Failed", "विफल"),
("Succeeded", "सफल"),
("Someone turns on privacy mode, exit", "किसी ने गोपनीयता मोड चालू किया है, बाहर निकल रहे हैं"),
("Unsupported", "असमर्थित"),
("Peer denied", "दूसरे सिस्टम ने मना कर दिया"),
("Please install plugins", "कृपया प्लगइन्स इंस्टॉल करें"),
("Peer exit", "दूसरा सिस्टम बाहर निकल गया"),
("Failed to turn off", "बंद करने में विफल"),
("Turned off", "बंद कर दिया गया"),
("Language", "भाषा"),
("Keep RustDesk background service", "RustDesk बैकग्राउंड सेवा चालू रखें"),
("Ignore Battery Optimizations", "बैटरी ऑप्टिमाइजेशन को अनदेखा करें"),
("android_open_battery_optimizations_tip", "डिस्कनेक्शन से बचने के लिए बैटरी ऑप्टिमाइजेशन सेटिंग खोलें"),
("Start on boot", "बूट पर शुरू करें"),
("Start the screen sharing service on boot, requires special permissions", "बूट पर स्क्रीन शेयरिंग सेवा शुरू करें, विशेष अनुमतियों की आवश्यकता है"),
("Connection not allowed", "कनेक्शन की अनुमति नहीं है"),
("Legacy mode", "लेगेसी (Legacy) मोड"),
("Map mode", "मैप मोड"),
("Translate mode", "अनुवाद मोड"),
("Use permanent password", "स्थायी पासवर्ड का उपयोग करें"),
("Use both passwords", "दोनों पासवर्ड का उपयोग करें"),
("Set permanent password", "स्थायी पासवर्ड सेट करें"),
("Enable remote restart", "रिमोट रीस्टार्ट सक्षम करें"),
("Restart remote device", "रिमोट डिवाइस रीस्टार्ट करें"),
("Are you sure you want to restart", "क्या आप वाकई रीस्टार्ट करना चाहते हैं?"),
("Restarting remote device", "रिमोट डिवाइस रीस्टार्ट हो रहा है"),
("remote_restarting_tip", "रिमोट डिवाइस रीस्टार्ट हो रहा है, कृपया प्रतीक्षा करें..."),
("Copied", "कॉपी किया गया"),
("Exit Fullscreen", "फुलस्क्रीन से बाहर निकलें"),
("Fullscreen", "फुलस्क्रीन"),
("Mobile Actions", "मोबाइल क्रियाएं"),
("Select Monitor", "मॉनिटर चुनें"),
("Control Actions", "नियंत्रण क्रियाएं"),
("Display Settings", "डिस्प्ले सेटिंग्स"),
("Ratio", "अनुपात (Ratio)"),
("Image Quality", "इमेज गुणवत्ता"),
("Scroll Style", "स्क्रॉल शैली"),
("Show Toolbar", "टूलबार दिखाएं"),
("Hide Toolbar", "टूलबार छुपाएं"),
("Direct Connection", "सीधा कनेक्शन"),
("Relay Connection", "रिले कनेक्शन"),
("Secure Connection", "सुरक्षित कनेक्शन"),
("Insecure Connection", "असुरक्षित कनेक्शन"),
("Scale original", "मूल पैमाना"),
("Scale adaptive", "अनुकूली पैमाना"),
("General", "सामान्य"),
("Security", "सुरक्षा"),
("Theme", "थीम"),
("Dark Theme", "डार्क थीम"),
("Light Theme", "लाइट थीम"),
("Dark", "डार्क"),
("Light", "लाइट"),
("Follow System", "सिस्टम का पालन करें"),
("Enable hardware codec", "हार्डवेयर कोडेक सक्षम करें"),
("Unlock Security Settings", "सुरक्षा सेटिंग्स अनलॉक करें"),
("Enable audio", "ऑडियो सक्षम करें"),
("Unlock Network Settings", "नेटवर्क सेटिंग्स अनलॉक करें"),
("Server", "सर्वर"),
("Direct IP Access", "सीधी IP पहुंच"),
("Proxy", "प्रॉक्सी"),
("Apply", "लागू करें"),
("Disconnect all devices?", "सभी डिवाइस डिस्कनेक्ट करें?"),
("Clear", "साफ करें"),
("Audio Input Device", "ऑडियो इनपुट डिवाइस"),
("Use IP Whitelisting", "IP श्वेतसूची का उपयोग करें"),
("Network", "नेटवर्क"),
("Pin Toolbar", "टूलबार पिन करें"),
("Unpin Toolbar", "टूलबार अनपिन करें"),
("Recording", "रिकॉर्डिंग"),
("Directory", "निर्देशिका"),
("Automatically record incoming sessions", "आने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"),
("Automatically record outgoing sessions", "जाने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"),
("Change", "बदलें"),
("Start session recording", "सत्र रिकॉर्डिंग शुरू करें"),
("Stop session recording", "सत्र रिकॉर्डिंग रोकें"),
("Enable recording session", "सत्र रिकॉर्डिंग सक्षम करें"),
("Enable LAN discovery", "LAN खोज सक्षम करें"),
("Deny LAN discovery", "LAN खोज अस्वीकार करें"),
("Write a message", "संदेश लिखें"),
("Prompt", "प्रॉम्प्ट"),
("Please wait for confirmation of UAC...", "कृपया UAC की पुष्टि की प्रतीक्षा करें..."),
("elevated_foreground_window_tip", "रिमोट डेस्कटॉप की वर्तमान विंडो को उच्च अनुमतियों की आवश्यकता है।"),
("Disconnected", "डिस्कनेक्ट हो गया"),
("Other", "अन्य"),
("Confirm before closing multiple tabs", "एकाधिक टैब बंद करने से पहले पुष्टि करें"),
("Keyboard Settings", "कीबोर्ड सेटिंग्स"),
("Full Access", "पूर्ण पहुंच (Full Access)"),
("Screen Share", "स्क्रीन शेयर"),
("ubuntu-21-04-required", "Ubuntu 21.04 या उच्चतर आवश्यक है"),
("wayland-requires-higher-linux-version", "Wayland के लिए उच्च Linux संस्करण आवश्यक है"),
("xdp-portal-unavailable", "XDP पोर्टल अनुपलब्ध है"),
("JumpLink", "JumpLink"),
("Please Select the screen to be shared(Operate on the peer side).", "कृपया साझा की जाने वाली स्क्रीन चुनें (दूसरे सिस्टम पर संचालित करें)।"),
("Show RustDesk", "RustDesk दिखाएं"),
("This PC", "यह PC"),
("or", "या"),
("Elevate", "एलीवेट (Elevate) करें"),
("Zoom cursor", "ज़ूम कर्सर"),
("Accept sessions via password", "पासवर्ड के माध्यम से सत्र स्वीकार करें"),
("Accept sessions via click", "क्लिक के माध्यम से सत्र स्वीकार करें"),
("Accept sessions via both", "दोनों के माध्यम से सत्र स्वीकार करें"),
("Please wait for the remote side to accept your session request...", "कृपया रिमोट साइड द्वारा आपके सत्र अनुरोध को स्वीकार करने की प्रतीक्षा करें..."),
("One-time Password", "वन-टाइम पासवर्ड"),
("Use one-time password", "वन-टाइम पासवर्ड का उपयोग करें"),
("One-time password length", "वन-टाइम पासवर्ड की लंबाई"),
("Request access to your device", "आपके डिवाइस तक पहुंच का अनुरोध"),
("Hide connection management window", "कनेक्शन प्रबंधन विंडो छुपाएं"),
("hide_cm_tip", "केवल तभी छुपाएं जब पासवर्ड से कनेक्शन की अनुमति हो"),
("wayland_experiment_tip", "Wayland समर्थन अभी परीक्षण मोड में है"),
("Right click to select tabs", "टैब चुनने के लिए राइट क्लिक करें"),
("Skipped", "छोड़ दिया गया"),
("Add to address book", "पता पुस्तिका में जोड़ें"),
("Group", "समूह"),
("Search", "खोजें"),
("Closed manually by web console", "वेब कंसोल द्वारा मैन्युअल रूप से बंद किया गया"),
("Local keyboard type", "स्थानीय कीबोर्ड प्रकार"),
("Select local keyboard type", "स्थानीय कीबोर्ड प्रकार चुनें"),
("software_render_tip", "यदि आपकी स्क्रीन काली है, तो इसे आज़माएं"),
("Always use software rendering", "हमेशा सॉफ़्टवेयर रेंडरिंग का उपयोग करें"),
("config_input", "इनपुट कॉन्फ़िगर करें"),
("config_microphone", "माइक्रोफ़ोन कॉन्फ़िगर करें"),
("request_elevation_tip", "रिमोट साइड से उच्च अनुमतियों का अनुरोध करें"),
("Wait", "प्रतीक्षा करें"),
("Elevation Error", "एलीवेशन (Elevation) त्रुटि"),
("Ask the remote user for authentication", "रिमोट उपयोगकर्ता से प्रमाणीकरण मांगें"),
("Choose this if the remote account is administrator", "यदि रिमोट खाता व्यवस्थापक (Admin) है तो इसे चुनें"),
("Transmit the username and password of administrator", "व्यवस्थापक का उपयोगकर्ता नाम और पासवर्ड भेजें"),
("still_click_uac_tip", "रिमोट उपयोगकर्ता को अभी भी UAC विंडो पर 'हाँ' क्लिक करना होगा।"),
("Request Elevation", "एलीवेशन का अनुरोध करें"),
("wait_accept_uac_tip", "कृपया रिमोट उपयोगकर्ता द्वारा UAC स्वीकार करने की प्रतीक्षा करें।"),
("Elevate successfully", "सफलतापूर्वक एलीवेट किया गया"),
("uppercase", "बड़े अक्षर (Uppercase)"),
("lowercase", "छोटे अक्षर (Lowercase)"),
("digit", "अंक (Digit)"),
("special character", "विशेष वर्ण"),
("length>=8", "लंबाई >= 8"),
("Weak", "कमजोर"),
("Medium", "मध्यम"),
("Strong", "मजबूत"),
("Switch Sides", "साइड्स बदलें"),
("Please confirm if you want to share your desktop?", "कृपया पुष्टि करें कि क्या आप अपना डेस्कटॉप साझा करना चाहते हैं?"),
("Display", "डिस्प्ले"),
("Default View Style", "डिफ़ॉल्ट व्यू शैली"),
("Default Scroll Style", "डिफ़ॉल्ट स्क्रॉल शैली"),
("Default Image Quality", "डिफ़ॉल्ट इमेज गुणवत्ता"),
("Default Codec", "डिफ़ॉल्ट कोडेक"),
("Bitrate", "बिटरेट"),
("FPS", "FPS"),
("Auto", "ऑटो"),
("Other Default Options", "अन्य डिफ़ॉल्ट विकल्प"),
("Voice call", "वॉयस कॉल"),
("Text chat", "टेक्स्ट चैट"),
("Stop voice call", "वॉयस कॉल बंद करें"),
("relay_hint_tip", "सीधा कनेक्शन संभव नहीं हो सकता; आप रिले के माध्यम से जुड़ने का प्रयास कर सकते हैं।"),
("Reconnect", "पुनः कनेक्ट करें"),
("Codec", "कोडेक"),
("Resolution", "रिज़ॉल्यूशन"),
("No transfers in progress", "कोई स्थानांतरण जारी नहीं है"),
("Set one-time password length", "वन-टाइम पासवर्ड की लंबाई सेट करें"),
("RDP Settings", "RDP सेटिंग्स"),
("Sort by", "इसके अनुसार क्रमबद्ध करें"),
("New Connection", "नया कनेक्शन"),
("Restore", "पुनर्स्थापित करें"),
("Minimize", "मिनिमाइज करें"),
("Maximize", "मैक्सिमाइज करें"),
("Your Device", "आपका डिवाइस"),
("empty_recent_tip", "हाल के सत्र यहाँ दिखाई देंगे।"),
("empty_favorite_tip", "पसंदीदा डिवाइस यहाँ दिखाई देंगे।"),
("empty_lan_tip", "खोजे गए डिवाइस यहाँ दिखाई देंगे।"),
("empty_address_book_tip", "आपके पता पुस्तिका में वर्तमान में कोई डिवाइस नहीं है।"),
("Empty Username", "खाली उपयोगकर्ता नाम"),
("Empty Password", "खाली पासवर्ड"),
("Me", "मैं"),
("identical_file_tip", "यह फ़ाइल पहले से ही मौजूद है।"),
("show_monitors_tip", "टूलबार में मॉनिटर दिखाएं"),
("View Mode", "व्यू मोड"),
("login_linux_tip", "रिमोट Linux सत्र शुरू करने के लिए आपको लॉगिन करना होगा"),
("verify_rustdesk_password_tip", "RustDesk पासवर्ड सत्यापित करें"),
("remember_account_tip", "इस खाते को याद रखें"),
("os_account_desk_tip", "रिमोट डेस्कटॉप को एक्सेस करने के लिए OS खाते का उपयोग करें"),
("OS Account", "OS खाता"),
("another_user_login_title_tip", "एक अन्य उपयोगकर्ता पहले से ही लॉगिन है"),
("another_user_login_text_tip", "डिस्कनेक्ट करें और पुनः प्रयास करें"),
("xorg_not_found_title_tip", "Xorg नहीं मिला"),
("xorg_not_found_text_tip", "कृपया Xorg इंस्टॉल करें"),
("no_desktop_title_tip", "कोई डेस्कटॉप उपलब्ध नहीं है"),
("no_desktop_text_tip", "कृपया Linux डेस्कटॉप इंस्टॉल करें"),
("No need to elevate", "एलीवेट करने की आवश्यकता नहीं है"),
("System Sound", "सिस्टम साउंड"),
("Default", "डिफ़ॉल्ट"),
("New RDP", "नया RDP"),
("Fingerprint", "फिंगरप्रिंट"),
("Copy Fingerprint", "फिंगरप्रिंट कॉपी करें"),
("no fingerprints", "कोई फिंगरप्रिंट नहीं"),
("Select a peer", "एक पीयर (Peer) चुनें"),
("Select peers", "पीयर्स चुनें"),
("Plugins", "प्लगइन्स"),
("Uninstall", "अनइंस्टॉल करें"),
("Update", "अपडेट करें"),
("Enable", "सक्षम करें"),
("Disable", "अक्षम करें"),
("Options", "विकल्प"),
("resolution_original_tip", "मूल रिज़ॉल्यूशन"),
("resolution_fit_local_tip", "स्थानीय स्क्रीन में फिट करें"),
("resolution_custom_tip", "कस्टम रिज़ॉल्यूशन"),
("Collapse toolbar", "टूलबार समेटें"),
("Accept and Elevate", "स्वीकार करें और एलीवेट करें"),
("accept_and_elevate_btn_tooltip", "कनेक्शन स्वीकार करें और UAC अनुमतियाँ मांगें।"),
("clipboard_wait_response_timeout_tip", "क्लिपबोर्ड प्रतिक्रिया के लिए समय समाप्त हो गया।"),
("Incoming connection", "आने वाला कनेक्शन"),
("Outgoing connection", "जाने वाला कनेक्शन"),
("Exit", "बाहर निकलें"),
("Open", "खोलें"),
("logout_tip", "क्या आप वाकई लॉगआउट करना चाहते हैं?"),
("Service", "सेवा"),
("Start", "शुरू करें"),
("Stop", "रोकें"),
("exceed_max_devices", "आप डिवाइस की अधिकतम सीमा को पार कर चुके हैं।"),
("Sync with recent sessions", "हाल के सत्रों के साथ सिंक करें"),
("Sort tags", "टैग क्रमबद्ध करें"),
("Open connection in new tab", "नये टैब में कनेक्शन खोलें"),
("Move tab to new window", "टैब को नयी विंडो में ले जाएं"),
("Can not be empty", "खाली नहीं हो सकता"),
("Already exists", "पहले से मौजूद है"),
("Change Password", "पासवर्ड बदलें"),
("Refresh Password", "पासवर्ड रिफ्रेश करें"),
("ID", "ID"),
("Grid View", "ग्रिड व्यू"),
("List View", "लिस्ट व्यू"),
("Select", "चुनें"),
("Toggle Tags", "टैग टॉगल करें"),
("pull_ab_failed_tip", "पता पुस्तिका अपडेट करने में विफल।"),
("push_ab_failed_tip", "सर्वर पर पता पुस्तिका सिंक करने में विफल।"),
("synced_peer_readded_tip", "हाल के सत्रों में मौजूद डिवाइस पता पुस्तिका में सिंक किए गए थे।"),
("Change Color", "रंग बदलें"),
("Primary Color", "प्राथमिक रंग"),
("HSV Color", "HSV रंग"),
("Installation Successful!", "इंस्टॉलेशन सफल रहा!"),
("Installation failed!", "इंस्टॉलेशन विफल रहा!"),
("Reverse mouse wheel", "माउस व्हील उल्टा करें"),
("{} sessions", "{} सत्र"),
("scam_title", "धोखाधड़ी की चेतावनी!"),
("scam_text1", "यदि आप किसी ऐसे व्यक्ति से बात कर रहे हैं जिसे आप नहीं जानते और जिसने आपसे RustDesk उपयोग करने को कहा है, तो तुरंत डिस्कनेक्ट कर दें।"),
("scam_text2", "यह एक घोटाला हो सकता है। अपना आईडी या पासवर्ड किसी को न दें।"),
("Don't show again", "दोबारा न दिखाएं"),
("I Agree", "मैं सहमत हूँ"),
("Decline", "अस्वीकार करें"),
("Timeout in minutes", "मिनटों में टाइमआउट"),
("auto_disconnect_option_tip", "निष्क्रियता पर स्वचालित रूप से डिस्कनेक्ट करें"),
("Connection failed due to inactivity", "निष्क्रियता के कारण कनेक्शन विफल रहा"),
("Check for software update on startup", "स्टार्टअप पर सॉफ़्टवेयर अपडेट की जांच करें"),
("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk सर्वर प्रो को संस्करण {} में अपग्रेड करें"),
("pull_group_failed_tip", "समूह खींचने (Pull) में विफल"),
("Filter by intersection", "इंटरसेक्शन द्वारा फ़िल्टर करें"),
("Remove wallpaper during incoming sessions", "आने वाले सत्रों के दौरान वॉलपेपर हटा दें"),
("Test", "परीक्षण"),
("display_is_plugged_out_msg", "डिस्प्ले हटा दिया गया है।"),
("No displays", "कोई डिस्प्ले नहीं"),
("Open in new window", "नयी विंडो में खोलें"),
("Show displays as individual windows", "डिस्प्ले को व्यक्तिगत विंडो के रूप में दिखाएं"),
("Use all my displays for the remote session", "रिमोट सत्र के लिए मेरे सभी डिस्प्ले का उपयोग करें"),
("selinux_tip", "डिवाइस पर SELinux सक्षम है।"),
("Change view", "व्यू बदलें"),
("Big tiles", "बड़ी टाइलें"),
("Small tiles", "छोटी टाइलें"),
("List", "लिस्ट"),
("Virtual display", "वर्चुअल डिस्प्ले"),
("Plug out all", "सभी अनप्लग करें"),
("True color (4:4:4)", "सच्चा रंग (4:4:4)"),
("Enable blocking user input", "उपयोगकर्ता इनपुट को ब्लॉक करना सक्षम करें"),
("id_input_tip", "आप ID, उपनाम (Alias) या IP पता दर्ज कर सकते हैं।"),
("privacy_mode_impl_mag_tip", "मैग्निफायर (Magnifier) गोपनीयता मोड"),
("privacy_mode_impl_virtual_display_tip", "वर्चुअल डिस्प्ले गोपनीयता मोड"),
("Enter privacy mode", "गोपनीयता मोड में प्रवेश करें"),
("Exit privacy mode", "गोपनीयता मोड से बाहर निकलें"),
("idd_not_support_under_win10_2004_tip", "वर्चुअल डिस्प्ले Windows 10 संस्करण 2004 या उच्चतर पर समर्थित है।"),
("input_source_1_tip", "इनपुट स्रोत 1"),
("input_source_2_tip", "इनपुट स्रोत 2"),
("Swap control-command key", "Control और Command कुंजियों को बदलें"),
("swap-left-right-mouse", "बाएं और दाएं माउस बटन को बदलें"),
("2FA code", "2FA कोड"),
("More", "अधिक"),
("enable-2fa-title", "द्वि-कारक प्रमाणीकरण (2FA) सक्षम करें"),
("enable-2fa-desc", "कृपया अपना ऑथेंटिकेटर ऐप सेट करें।"),
("wrong-2fa-code", "गलत 2FA कोड।"),
("enter-2fa-title", "2FA कोड दर्ज करें"),
("Email verification code must be 6 characters.", "ईमेल सत्यापन कोड 6 अक्षरों का होना चाहिए।"),
("2FA code must be 6 digits.", "2FA कोड 6 अंकों का होना चाहिए।"),
("Multiple Windows sessions found", "एकाधिक Windows सत्र मिले"),
("Please select the session you want to connect to", "कृपया वह सत्र चुनें जिससे आप जुड़ना चाहते हैं"),
("powered_by_me", "मेरे द्वारा संचालित"),
("outgoing_only_desk_tip", "यह केवल आउटगोइंग मोड है"),
("preset_password_warning", "सुरक्षा के लिए, कृपया डिफ़ॉल्ट पासवर्ड बदलें।"),
("Security Alert", "सुरक्षा चेतावनी"),
("My address book", "मेरी पता पुस्तिका"),
("Personal", "व्यक्तिगत"),
("Owner", "स्वामी"),
("Set shared password", "साझा पासवर्ड सेट करें"),
("Exist in", "इसमें मौजूद है"),
("Read-only", "केवल पढ़ने के लिए"),
("Read/Write", "पढ़ना/लिखना"),
("Full Control", "पूर्ण नियंत्रण"),
("share_warning_tip", "सावधानी: आप अपना एक्सेस साझा कर रहे हैं।"),
("Everyone", "हर कोई"),
("ab_web_console_tip", "वेब कंसोल पता पुस्तिका"),
("allow-only-conn-window-open-tip", "केवल तभी कनेक्शन की अनुमति दें जब RustDesk विंडो खुली हो"),
("no_need_privacy_mode_no_physical_displays_tip", "कोई भौतिक डिस्प्ले नहीं मिला, गोपनीयता मोड की आवश्यकता नहीं है।"),
("Follow remote cursor", "रिमोट कर्सर का पालन करें"),
("Follow remote window focus", "रिमोट विंडो फोकस का पालन करें"),
("default_proxy_tip", "डिफ़ॉल्ट प्रॉक्सी सेटिंग"),
("no_audio_input_device_tip", "कोई ऑडियो इनपुट डिवाइस नहीं मिला।"),
("Incoming", "आने वाली"),
("Outgoing", "जाने वाली"),
("Clear Wayland screen selection", "Wayland स्क्रीन चयन साफ़ करें"),
("clear_Wayland_screen_selection_tip", "Wayland के स्क्रीन चयन को रीसेट करें।"),
("confirm_clear_Wayland_screen_selection_tip", "क्या आप वाकई स्क्रीन चयन साफ़ करना चाहते हैं?"),
("android_new_voice_call_tip", "नया वॉयस कॉल अनुरोध"),
("texture_render_tip", "टेक्सचर रेंडरिंग का उपयोग करें"),
("Use texture rendering", "टेक्सचर रेंडरिंग का उपयोग करें"),
("Floating window", "फ्लोटिंग विंडो"),
("floating_window_tip", "बैकग्राउंड में रहने के दौरान RustDesk को दिखाएं"),
("Keep screen on", "स्क्रीन चालू रखें"),
("Never", "कभी नहीं"),
("During controlled", "नियंत्रण के दौरान"),
("During service is on", "जब सेवा चालू हो"),
("Capture screen using DirectX", "DirectX का उपयोग करके स्क्रीन कैप्चर करें"),
("Back", "पीछे"),
("Apps", "ऐप्स"),
("Volume up", "आवाज़ बढ़ाएं"),
("Volume down", "आवाज़ कम करें"),
("Power", "पावर"),
("Telegram bot", "Telegram बॉट"),
("enable-bot-tip", "सूचनाओं के लिए बोट सक्षम करें"),
("enable-bot-desc", "निर्देशों के लिए हमारे टेलीग्राम बोट को देखें।"),
("cancel-2fa-confirm-tip", "क्या आप वाकई 2FA रद्द करना चाहते हैं?"),
("cancel-bot-confirm-tip", "क्या आप वाकई बोट रद्द करना चाहते हैं?"),
("About RustDesk", "RustDesk के बारे में"),
("Send clipboard keystrokes", "क्लिपबोर्ड कीस्ट्रोक्स भेजें"),
("network_error_tip", "नेटवर्क कनेक्शन त्रुटि, कृपया पुनः प्रयास करें।"),
("Unlock with PIN", "PIN से अनलॉक करें"),
("Requires at least {} characters", "कम से कम {} अक्षरों की आवश्यकता है"),
("Wrong PIN", "गलत PIN"),
("Set PIN", "PIN सेट करें"),
("Enable trusted devices", "विश्वसनीय डिवाइस सक्षम करें"),
("Manage trusted devices", "विश्वसनीय डिवाइस प्रबंधित करें"),
("Platform", "प्लेटफ़ॉर्म"),
("Days remaining", "शेष दिन"),
("enable-trusted-devices-tip", "केवल विश्वसनीय डिवाइस ही पासवर्ड के बिना जुड़ सकते हैं"),
("Parent directory", "पैरेंट निर्देशिका"),
("Resume", "फिर से शुरू करें"),
("Invalid file name", "अमान्य फ़ाइल नाम"),
("one-way-file-transfer-tip", "केवल एकतरफा फ़ाइल स्थानांतरण की अनुमति है"),
("Authentication Required", "प्रमाणीकरण आवश्यक"),
("Authenticate", "प्रमाणित करें"),
("web_id_input_tip", "रिमोट आईडी दर्ज करें"),
("Download", "डाउनलोड करें"),
("Upload folder", "फ़ोल्डर अपलोड करें"),
("Upload files", "फाइलें अपलोड करें"),
("Clipboard is synchronized", "क्लिपबोर्ड सिंक हो गया है"),
("Update client clipboard", "क्लाइंट क्लिपबोर्ड अपडेट करें"),
("Untagged", "बिना टैग वाला"),
("new-version-of-{}-tip", "{} का नया संस्करण उपलब्ध है"),
("Accessible devices", "सुलभ डिवाइस"),
("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"),
("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"),
("Printer", "प्रिंटर"),
("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"),
("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"),
("printer-{}-not-installed-tip", "प्रिंटर {} इंस्टॉल नहीं है।"),
("printer-{}-ready-tip", "प्रिंटर {} तैयार है।"),
("Install {} Printer", "{} प्रिंटर इंस्टॉल करें"),
("Outgoing Print Jobs", "आउटगोइंग प्रिंट कार्य"),
("Incoming Print Jobs", "इनकमिंग प्रिंट कार्य"),
("Incoming Print Job", "इनकमिंग प्रिंट कार्य"),
("use-the-default-printer-tip", "डिफ़ॉल्ट प्रिंटर का उपयोग करें"),
("use-the-selected-printer-tip", "चयनित प्रिंटर का उपयोग करें"),
("auto-print-tip", "स्वचालित रूप से प्रिंट करें"),
("print-incoming-job-confirm-tip", "प्रिंट कार्य स्वीकार करने से पहले पुष्टि करें"),
("remote-printing-disallowed-tile-tip", "रिमोट प्रिंटिंग की अनुमति नहीं है"),
("remote-printing-disallowed-text-tip", "कृपया सेटिंग्स में रिमोट प्रिंटिंग सक्षम करें।"),
("save-settings-tip", "सेटिंग्स सुरक्षित करें"),
("dont-show-again-tip", "दोबारा न दिखाएं"),
("Take screenshot", "स्क्रीनशॉट लें"),
("Taking screenshot", "स्क्रीनशॉट लिया जा रहा है"),
("screenshot-merged-screen-not-supported-tip", "मर्ज की गई स्क्रीन के स्क्रीनशॉट समर्थित नहीं हैं।"),
("screenshot-action-tip", "स्क्रीनशॉट लेने के बाद की कार्रवाई"),
("Save as", "इस रूप में सहेजें"),
("Copy to clipboard", "क्लिपबोर्ड पर कॉपी करें"),
("Enable remote printer", "रिमोट प्रिंटर सक्षम करें"),
("Downloading {}", "{} डाउनलोड हो रहा है"),
("{} Update", "{} अपडेट"),
("{}-to-update-tip", "अपडेट करने के लिए {}"),
("download-new-version-failed-tip", "नया संस्करण डाउनलोड करने में विफल।"),
("Auto update", "ऑटो अपडेट"),
("update-failed-check-msi-tip", "अपडेट विफल, कृपया MSI फ़ाइल की जांच करें।"),
("websocket_tip", "यदि पोर्ट ब्लॉक हैं, तो WebSocket का उपयोग करें।"),
("Use WebSocket", "WebSocket का उपयोग करें"),
("Trackpad speed", "ट्रैकपैड गति"),
("Default trackpad speed", "डिफ़ॉल्ट ट्रैकपैड गति"),
("Numeric one-time password", "संख्यात्मक वन-टाइम पासवर्ड"),
("Enable IPv6 P2P connection", "IPv6 P2P कनेक्शन सक्षम करें"),
("Enable UDP hole punching", "UDP होल पंचिंग सक्षम करें"),
("View camera", "कैमरा देखें"),
("Enable camera", "कैमरा सक्षम करें"),
("No cameras", "कोई कैमरा नहीं मिला"),
("view_camera_unsupported_tip", "रिमोट कैमरा समर्थित नहीं है।"),
("Terminal", "टर्मिनल"),
("Enable terminal", "टर्मिनल सक्षम करें"),
("New tab", "नया टैब"),
("Keep terminal sessions on disconnect", "डिस्कनेक्ट होने पर टर्मिनल सत्र चालू रखें"),
("Terminal (Run as administrator)", "टर्मिनल (प्रशासक के रूप में चलाएं)"),
("terminal-admin-login-tip", "प्रशासक लॉगिन आवश्यक है।"),
("Failed to get user token.", "उपयोगकर्ता टोकन प्राप्त करने में विफल।"),
("Incorrect username or password.", "गलत उपयोगकर्ता नाम या पासवर्ड।"),
("The user is not an administrator.", "उपयोगकर्ता प्रशासक नहीं है।"),
("Failed to check if the user is an administrator.", "जांचने में विफल कि क्या उपयोगकर्ता व्यवस्थापक है।"),
("Supported only in the installed version.", "केवल इंस्टॉल किए गए संस्करण में समर्थित।"),
("elevation_username_tip", "प्रशासक उपयोगकर्ता नाम दर्ज करें"),
("Preparing for installation ...", "स्थापना की तैयारी..."),
("Show my cursor", "मेरा कर्सर दिखाएं"),
("Scale custom", "कस्टम पैमाना"),
("Custom scale slider", "कस्टम स्केल स्लाइडर"),
("Decrease", "घटाएं"),
("Increase", "बढ़ाएं"),
("Show virtual mouse", "वर्चुअल माउस दिखाएं"),
("Virtual mouse size", "वर्चुअल माउस का आकार"),
("Small", "छोटा"),
("Large", "बड़ा"),
("Show virtual joystick", "वर्चुअल जॉयस्टिक दिखाएं"),
("Edit note", "नोट संपादित करें"),
("Alias", "उपनाम (Alias)"),
("ScrollEdge", "किनारे से स्क्रॉल"),
("Allow insecure TLS fallback", "असुरक्षित TLS फ़ालबैक की अनुमति दें"),
("allow-insecure-tls-fallback-tip", "पुराने सर्वर कनेक्शन के लिए उपयोग करें।"),
("Disable UDP", "UDP अक्षम करें"),
("disable-udp-tip", "कनेक्शन समस्याओं के लिए UDP बंद करें।"),
("server-oss-not-support-tip", "OSS सर्वर इसका समर्थन नहीं करता।"),
("input note here", "यहाँ नोट दर्ज करें"),
("note-at-conn-end-tip", "कनेक्शन के अंत में नोट दिखाएं"),
("Show terminal extra keys", "टर्मिनल की अतिरिक्त कुंजियाँ दिखाएं"),
("Relative mouse mode", "सापेक्ष (Relative) माउस मोड"),
("rel-mouse-not-supported-peer-tip", "रिमोट साइड पर समर्थित नहीं है।"),
("rel-mouse-not-ready-tip", "तैयार नहीं है।"),
("rel-mouse-lock-failed-tip", "माउस लॉक विफल।"),
("rel-mouse-exit-{}-tip", "बाहर निकलने के लिए {} दबाएं"),
("rel-mouse-permission-lost-tip", "अनुमति खो गई।"),
("Changelog", "परिवर्तन सूची (Changelog)"),
("keep-awake-during-outgoing-sessions-label", "आउटगोइंग सत्र के दौरान जागते रहें"),
("keep-awake-during-incoming-sessions-label", "इनकमिंग सत्र के दौरान जागते रहें"),
("Continue with {}", "{} के साथ जारी रखें"),
("Display Name", "प्रदर्शित नाम"),
("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"),
("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"),
].iter().cloned().collect();
}

View File

@@ -57,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ID Server", "ID-kiszolgáló"),
("Relay Server", "Továbbító-kiszolgáló"),
("API Server", "API-kiszolgáló"),
("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."),
("invalid_http", "A címnek mindenképpen http(s)://-rel kell kezdődnie."),
("Invalid IP", "A megadott IP-cím érvénytelen"),
("Invalid format", "Érvénytelen formátum"),
("server_not_support", "A kiszolgáló nem támogatja"),
@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
("Configure", "Beállítás"),
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a Képernyőfelvétel jogosultságot."),
("Installing ...", "Telepítés ..."),
("Install", "Telepítse"),
("Installation", "Telepítés"),
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Elfogadás?"),
("Open System Setting", "Rendszerbeállítások megnyitása"),
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a Hozzáférhetőség szolgáltatás használatát."),
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a RustDesk Input szolgáltatást."),
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a Kapcsolási szolgáltatás indítása gombra, vagy aktiválja a Képernyőfelvétel engedélyt."),
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
("Account", "Fiók"),
("Overwrite", "Felülírás"),
@@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a Bemenet figyelése jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a Hangfelvétel jogosultságot."),
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
("Wait", "Várjon"),
("Elevation Error", "Emelt szintű hozzáférési hiba"),
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("Request Elevation", "Emelt szintű jogok igénylése"),
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
("Elevate successfully", "Emelt szintű jogok megadva"),
@@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Hanghívás"),
("Text chat", "Szöveges csevegés"),
("Stop voice call", "Hanghívás leállítása"),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a Mindig továbbító-kiszolgálón keresztül kapcsolódom opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("Reconnect", "Újrakapcsolódás"),
("Codec", "Kodek"),
("Resolution", "Felbontás"),
@@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Kapcsolja ki az összeset"),
("True color (4:4:4)", "Valódi szín (4:4:4)"),
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például 9123456234/r."),
("privacy_mode_impl_mag_tip", "1. mód"),
("privacy_mode_impl_virtual_display_tip", "2. mód"),
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Power", "Főkapcsoló"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a /newbot parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
("About RustDesk", "A RustDesk névjegye"),
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
("Authentication Required", "Hitelesítés szükséges"),
("Authenticate", "Hitelesítés"),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg az „<id>@public” kulcsot. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("Download", "Letöltés"),
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
@@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Downloading {}", "{} letöltése"),
("{} Update", "{} frissítés"),
("{}-to-update-tip", "{} bezárása és az új verzió telepítése."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a Letöltés gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("Auto update", "Automatikus frissítés"),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a Letöltés gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
("Use WebSocket", "WebSocket használata"),
("Trackpad speed", "Érintőpad sebessége"),
@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
("Continue with {}", "Folytatás ezzel: {}"),
("Display Name", "Kijelző név"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
("Continue with {}", "Continua con {}"),
("Display Name", "Visualizza nome"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "È impostata una password permanente (nascosta)."),
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
].iter().cloned().collect();
}

View File

@@ -661,9 +661,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("printer-{}-not-installed-tip", "{} のプリンターがインストールされていません。"),
("printer-{}-ready-tip", "{} のプリンターがインストールされ、使用可能になっています。"),
("Install {} Printer", " {} のプリンターをインストール"),
("Outgoing Print Jobs", "送信印刷ジョブ"),
("Incoming Print Jobs", "受信印刷ジョブ"),
("Incoming Print Job", "受信印刷ジョブ"),
("Outgoing Print Jobs", "印刷ジョブの送信"),
("Incoming Print Jobs", "印刷ジョブの受信"),
("Incoming Print Job", "印刷ジョブの受信"),
("use-the-default-printer-tip", "既定のプリンターを使用する"),
("use-the-selected-printer-tip", "選択したプリンターを使用する"),
("auto-print-tip", "選択したプリンターを使用して自動的に印刷する"),
@@ -710,7 +710,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"),
("Preparing for installation ...", "インストールの準備中です..."),
("Show my cursor", "自分のカーソルを表示する"),
("Scale custom", "カスタムスケーリング"),
("Scale custom", "カスタムスケー"),
("Custom scale slider", "カスタムスケールのスライダー"),
("Decrease", "縮小"),
("Increase", "拡大"),
@@ -730,18 +730,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "ここにメモを入力"),
("note-at-conn-end-tip", "接続終了時にメモを要求する"),
("Show terminal extra keys", "ターミナルの追加キーを表示する"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "{} で続行"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Relative mouse mode", "相対マウスモード"),
("rel-mouse-not-supported-peer-tip", "接続先のデバイスは相対マウスモードに対応していません。"),
("rel-mouse-not-ready-tip", "相対マウスモードはまだ準備できていません。再度お試しください。"),
("rel-mouse-lock-failed-tip", "カーソルをロックできませんでした。相対マウスモードは無効化されています。"),
("rel-mouse-exit-{}-tip", "「{}」を押して終了します。"),
("rel-mouse-permission-lost-tip", "キーボード操作の権限が取り消されました。相対マウスモードは無効化されています。"),
("Changelog", "更新履歴"),
("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"),
("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"),
("Continue with {}", "{}で続行する"),
("Display Name", "表示名"),
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
("Continue with {}", "{}(으)로 계속"),
("Display Name", "표시 이름"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
].iter().cloned().collect();
}

746
src/lang/ml.rs Normal file
View File

@@ -0,0 +1,746 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "നില"),
("Your Desktop", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ്"),
("desk_tip", "ഈ ഐഡിയും പാസ്‌വേഡും ഉപയോഗിച്ച് നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് ആക്‌സസ് ചെയ്യാം."),
("Password", "പാസ്‌വേഡ്"),
("Ready", "തയ്യാറാണ്"),
("Established", "ബന്ധം സ്ഥാപിച്ചു"),
("connecting_status", "നെറ്റ്‌വർക്കുമായി ബന്ധിപ്പിക്കുന്നു..."),
("Enable service", "സർവീസ് പ്രവർത്തനക്ഷമമാക്കുക"),
("Start service", "സർവീസ് തുടങ്ങുക"),
("Service is running", "സർവീസ് പ്രവർത്തിക്കുന്നു"),
("Service is not running", "സർവീസ് പ്രവർത്തിക്കുന്നില്ല"),
("not_ready_status", "തയ്യാറായിട്ടില്ല. ദയവായി നിങ്ങളുടെ കണക്ഷൻ പരിശോധിക്കുക"),
("Control Remote Desktop", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് നിയന്ത്രിക്കുക"),
("Transfer file", "ഫയൽ കൈമാറുക"),
("Connect", "കണക്ട് ചെയ്യുക"),
("Recent sessions", "സമീപകാല സെഷനുകൾ"),
("Address book", "അഡ്രസ് ബുക്ക്"),
("Confirmation", "സ്ഥിരീകരണം"),
("TCP tunneling", "TCP ടണലിംഗ്"),
("Remove", "നീക്കം ചെയ്യുക"),
("Refresh random password", "പുതിയ പാസ്‌വേഡ് ജനറേറ്റ് ചെയ്യുക"),
("Set your own password", "സ്വന്തം പാസ്‌വേഡ് സെറ്റ് ചെയ്യുക"),
("Enable keyboard/mouse", "കീബോർഡ്/മൗസ് അനുവദിക്കുക"),
("Enable clipboard", "ക്ലിപ്പ്ബോർഡ് അനുവദിക്കുക"),
("Enable file transfer", "ഫയൽ കൈമാറ്റം അനുവദിക്കുക"),
("Enable TCP tunneling", "TCP ടണലിംഗ് അനുവദിക്കുക"),
("IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ്"),
("ID/Relay Server", "ID/റിലേ സെർവർ"),
("Import server config", "സെർവർ കോൺഫിഗറേഷൻ ഇമ്പോർട്ട് ചെയ്യുക"),
("Export Server Config", "സെർവർ കോൺഫിഗറേഷൻ എക്‌സ്‌പോർട്ട് ചെയ്യുക"),
("Import server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി ഇമ്പോർട്ട് ചെയ്തു"),
("Export server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി എക്‌സ്‌പോർട്ട് ചെയ്തു"),
("Invalid server configuration", "അസാധുവായ സെർവർ കോൺഫിഗറേഷൻ"),
("Clipboard is empty", "ക്ലിപ്പ്ബോർഡ് ശൂന്യമാണ്"),
("Stop service", "സർവീസ് നിർത്തുക"),
("Change ID", "ഐഡി മാറ്റുക"),
("Your new ID", "നിങ്ങളുടെ പുതിയ ഐഡി"),
("length %min% to %max%", "നീളം %min% മുതൽ %max% വരെ"),
("starts with a letter", "അക്ഷരത്തിൽ തുടങ്ങണം"),
("allowed characters", "അനുവദനീയമായ അക്ഷരങ്ങൾ"),
("id_change_tip", "ഐഡി മാറ്റിയാൽ നിലവിലുള്ള കണക്ഷൻ വിച്ഛേദിക്കപ്പെടും."),
("Website", "വെബ്സൈറ്റ്"),
("About", "വിവരങ്ങൾ"),
("Slogan_tip", "മികച്ച അനുഭവത്തിനായി നിർമ്മിച്ച റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ"),
("Privacy Statement", "സ്വകാര്യതാ പ്രസ്താവന"),
("Mute", "നിശബ്ദമാക്കുക"),
("Build Date", "നിർമ്മാണ തീയതി"),
("Version", "പതിപ്പ്"),
("Home", "ഹോം"),
("Audio Input", "ഓഡിയോ ഇൻപുട്ട്"),
("Enhancements", "മെച്ചപ്പെടുത്തലുകൾ"),
("Hardware Codec", "ഹാർഡ്‌വെയർ കോഡെക്"),
("Adaptive bitrate", "അഡാപ്റ്റീവ് ബിറ്റ്റേറ്റ്"),
("ID Server", "ID സെർവർ"),
("Relay Server", "റിലേ സെർവർ"),
("API Server", "API സെർവർ"),
("invalid_http", "അസാധുവായ HTTP ലിങ്ക്"),
("Invalid IP", "അസാധുവായ IP"),
("Invalid format", "അസാധുവായ ഫോർമാറ്റ്"),
("server_not_support", "സെർവർ പിന്തുണയ്ക്കുന്നില്ല"),
("Not available", "ലഭ്യമല്ല"),
("Too frequent", "അമിതമായ തവണകൾ"),
("Cancel", "റദ്ദാക്കുക"),
("Skip", "ഒഴിവാക്കുക"),
("Close", "അടയ്ക്കുക"),
("Retry", "വീണ്ടും ശ്രമിക്കുക"),
("OK", "ശരി"),
("Password Required", "പാസ്‌വേഡ് ആവശ്യമാണ്"),
("Please enter your password", "ദയവായി നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"),
("Remember password", "പാസ്‌വേഡ് ഓർമ്മിക്കുക"),
("Wrong Password", "തെറ്റായ പാസ്‌വേഡ്"),
("Do you want to enter again?", "നിങ്ങൾക്ക് വീണ്ടും ശ്രമിക്കണോ?"),
("Connection Error", "കണക്ഷൻ പിശക്"),
("Error", "പിശക്"),
("Reset by the peer", "മറുഭാഗത്തുനിന്ന് റീസെറ്റ് ചെയ്തു"),
("Connecting...", "ബന്ധിപ്പിക്കുന്നു..."),
("Connection in progress. Please wait.", "കണക്ഷൻ നടക്കുന്നു. ദയവായി കാത്തിരിക്കുക."),
("Please try 1 minute later", "ദയവായി ഒരു മിനിറ്റിന് ശേഷം ശ്രമിക്കുക"),
("Login Error", "ലോഗിൻ പിശക്"),
("Successful", "വിജയിച്ചു"),
("Connected, waiting for image...", "ബന്ധിപ്പിച്ചു, ചിത്രത്തിനായി കാത്തിരിക്കുന്നു..."),
("Name", "പേര്"),
("Type", "തരം"),
("Modified", "മാറ്റം വരുത്തിയത്"),
("Size", "വലിപ്പം"),
("Show Hidden Files", "മറഞ്ഞിരിക്കുന്ന ഫയലുകൾ കാണിക്കുക"),
("Receive", "സ്വീകരിക്കുക"),
("Send", "അയക്കുക"),
("Refresh File", "ഫയൽ പുതുക്കുക"),
("Local", "ലോക്കൽ"),
("Remote", "റിമോട്ട്"),
("Remote Computer", "റിമോട്ട് കമ്പ്യൂട്ടർ"),
("Local Computer", "ലോക്കൽ കമ്പ്യൂട്ടർ"),
("Confirm Delete", "ഡിലീറ്റ് ചെയ്യുന്നത് സ്ഥിരീകരിക്കുക"),
("Delete", "ഡിലീറ്റ് ചെയ്യുക"),
("Properties", "പ്രോപ്പർട്ടീസ്"),
("Multi Select", "ഒന്നിലധികം തിരഞ്ഞെടുക്കുക"),
("Select All", "എല്ലാം തിരഞ്ഞെടുക്കുക"),
("Unselect All", "തിരഞ്ഞെടുത്തവ ഒഴിവാക്കുക"),
("Empty Directory", "ശൂന്യമായ ഡയറക്ടറി"),
("Not an empty directory", "ഡയറക്ടറി ശൂന്യമല്ല"),
("Are you sure you want to delete this file?", "ഈ ഫയൽ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Are you sure you want to delete this empty directory?", "ഈ ശൂന്യമായ ഡയറക്ടറി ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Are you sure you want to delete the file of this directory?", "ഈ ഡയറക്ടറിയിലെ ഫയലുകൾ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Do this for all conflicts", "എല്ലാ വൈരുദ്ധ്യങ്ങൾക്കും ഇതുതന്നെ ചെയ്യുക"),
("This is irreversible!", "ഇത് പഴയപടിയാക്കാൻ കഴിയില്ല!"),
("Deleting", "ഡിലീറ്റ് ചെയ്യുന്നു"),
("files", "ഫയലുകൾ"),
("Waiting", "കാത്തിരിക്കുന്നു"),
("Finished", "പൂർത്തിയായി"),
("Speed", "വേഗത"),
("Custom Image Quality", "ഇമേജ് ക്വാളിറ്റി മാറ്റുക"),
("Privacy mode", "സ്വകാര്യ മോഡ്"),
("Block user input", "യൂസർ ഇൻപുട്ട് തടയുക"),
("Unblock user input", "യൂസർ ഇൻപുട്ട് അനുവദിക്കുക"),
("Adjust Window", "വിൻഡോ ക്രമീകരിക്കുക"),
("Original", "ഒറിജിനൽ"),
("Shrink", "ചുരുക്കുക"),
("Stretch", "വലിപ്പിക്കുക"),
("Scrollbar", "സ്ക്രോൾബാർ"),
("ScrollAuto", "ഓട്ടോ സ്ക്രോൾ"),
("Good image quality", "നല്ല ക്വാളിറ്റി"),
("Balanced", "സന്തുലിതം"),
("Optimize reaction time", "പ്രതികരണ സമയം മെച്ചപ്പെടുത്തുക"),
("Custom", "കസ്റ്റം"),
("Show remote cursor", "റിമോട്ട് കർസർ കാണിക്കുക"),
("Show quality monitor", "ക്വാളിറ്റി മോണിറ്റർ കാണിക്കുക"),
("Disable clipboard", "ക്ലിപ്പ്ബോർഡ് ഒഴിവാക്കുക"),
("Lock after session end", "സെഷൻ കഴിഞ്ഞാൽ ലോക്ക് ചെയ്യുക"),
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del നൽകുക"),
("Insert Lock", "ലോക്ക് ചെയ്യുക"),
("Refresh", "പുതുക്കുക"),
("ID does not exist", "ഐഡി നിലവിലില്ല"),
("Failed to connect to rendezvous server", "സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Please try later", "ദയവായി പിന്നീട് ശ്രമിക്കുക"),
("Remote desktop is offline", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് ഓഫ്‌ലൈനാണ്"),
("Key mismatch", "കീ പൊരുത്തക്കേട്"),
("Timeout", "സമയം കഴിഞ്ഞു"),
("Failed to connect to relay server", "റിലേ സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Failed to connect via rendezvous server", "സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Failed to connect via relay server", "റിലേ സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Failed to make direct connection to remote desktop", "നേരിട്ട് ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Set Password", "പാസ്‌വേഡ് നൽകുക"),
("OS Password", "OS പാസ്‌വേഡ്"),
("install_tip", "മികച്ച പ്രകടനത്തിനായി ഇൻസ്റ്റാൾ ചെയ്യുക."),
("Click to upgrade", "അപ്‌ഗ്രേഡ് ചെയ്യാൻ ക്ലിക്ക് ചെയ്യുക"),
("Configure", "ക്രമീകരിക്കുക"),
("config_acc", "അക്‌സസിബിലിറ്റി ക്രമീകരിക്കുക"),
("config_screen", "സ്ക്രീൻ ക്രമീകരിക്കുക"),
("Installing ...", "ഇൻസ്റ്റാൾ ചെയ്യുന്നു..."),
("Install", "ഇൻസ്റ്റാൾ ചെയ്യുക"),
("Installation", "ഇൻസ്റ്റാളേഷൻ"),
("Installation Path", "ഇൻസ്റ്റാളേഷൻ പാത്ത്"),
("Create start menu shortcuts", "സ്റ്റാർട്ട് മെനുവിൽ ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"),
("Create desktop icon", "ഡെസ്ക്ടോപ്പ് ഐക്കൺ ഉണ്ടാക്കുക"),
("agreement_tip", "ഇൻസ്റ്റാൾ ചെയ്യുന്നതിലൂടെ നിങ്ങൾ കരാറുകൾ അംഗീകരിക്കുന്നു."),
("Accept and Install", "അംഗീകരിച്ച് ഇൻസ്റ്റാൾ ചെയ്യുക"),
("End-user license agreement", "ലൈസൻസ് കരാർ"),
("Generating ...", "ഉണ്ടാക്കുന്നു..."),
("Your installation is lower version.", "നിങ്ങളുടെ ഇൻസ്റ്റാളേഷൻ പഴയ പതിപ്പാണ്."),
("not_close_tcp_tip", "ടണൽ ഉപയോഗിക്കുമ്പോൾ ഈ വിൻഡോ അടയ്ക്കരുത്."),
("Listening ...", "ശ്രദ്ധിക്കുന്നു..."),
("Remote Host", "റിമോട്ട് ഹോസ്റ്റ്"),
("Remote Port", "റിമോട്ട് പോർട്ട്"),
("Action", "നടപടി"),
("Add", "ചേർക്കുക"),
("Local Port", "ലോക്കൽ പോർട്ട്"),
("Local Address", "ലോക്കൽ അഡ്രസ്"),
("Change Local Port", "ലോക്കൽ പോർട്ട് മാറ്റുക"),
("setup_server_tip", "വേഗതയുള്ള കണക്ഷനായി സ്വന്തം സെർവർ സജ്ജമാക്കുക"),
("Too short, at least 6 characters.", "വളരെ ചെറുതാണ്, കുറഞ്ഞത് 6 അക്ഷരങ്ങൾ വേണം."),
("The confirmation is not identical.", "സ്ഥിരീകരണം ഒരേപോലെയല്ല."),
("Permissions", "അനുമതികൾ"),
("Accept", "സ്വീകരിക്കുക"),
("Dismiss", "നിരസിക്കുക"),
("Disconnect", "വിച്ഛേദിക്കുക"),
("Enable file copy and paste", "ഫയൽ കോപ്പി-പേസ്റ്റ് അനുവദിക്കുക"),
("Connected", "ബന്ധിപ്പിച്ചു"),
("Direct and encrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്തതുമായ കണക്ഷൻ"),
("Relayed and encrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്ത കണക്ഷൻ"),
("Direct and unencrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്യാത്തതുമായ കണക്ഷൻ"),
("Relayed and unencrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്യാത്ത കണക്ഷൻ"),
("Enter Remote ID", "റിമോട്ട് ഐഡി നൽകുക"),
("Enter your password", "നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"),
("Logging in...", "ലോഗിൻ ചെയ്യുന്നു..."),
("Enable RDP session sharing", "RDP സെഷൻ പങ്കിടൽ അനുവദിക്കുക"),
("Auto Login", "ഓട്ടോ ലോഗിൻ"),
("Enable direct IP access", "നേരിട്ടുള്ള IP ആക്‌സസ് അനുവദിക്കുക"),
("Rename", "പേര് മാറ്റുക"),
("Space", "സ്പേസ്"),
("Create desktop shortcut", "ഡെസ്ക്ടോപ്പ് ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"),
("Change Path", "പാത്ത് മാറ്റുക"),
("Create Folder", "ഫോൾഡർ ഉണ്ടാക്കുക"),
("Please enter the folder name", "ദയവായി ഫോൾഡറിന്റെ പേര് നൽകുക"),
("Fix it", "പരിഹരിക്കുക"),
("Warning", "മുന്നറിയിപ്പ്"),
("Login screen using Wayland is not supported", "Wayland വഴിയുള്ള ലോഗിൻ സപ്പോർട്ട് ചെയ്യുന്നില്ല"),
("Reboot required", "റീബൂട്ട് ആവശ്യമാണ്"),
("Unsupported display server", "പിന്തുണയ്ക്കാത്ത ഡിസ്‌പ്ലേ സെർവർ"),
("x11 expected", "x11 ആവശ്യമാണ്"),
("Port", "പോർട്ട്"),
("Settings", "ക്രമീകരണങ്ങൾ"),
("Username", "യൂസർ നെയിം"),
("Invalid port", "അസാധുവായ പോർട്ട്"),
("Closed manually by the peer", "മറുഭാഗത്തുനിന്നും മാനുവലായി അടച്ചു"),
("Enable remote configuration modification", "റിമോട്ട് കോൺഫിഗറേഷൻ മാറ്റങ്ങൾ അനുവദിക്കുക"),
("Run without install", "ഇൻസ്റ്റാൾ ചെയ്യാതെ പ്രവർത്തിപ്പിക്കുക"),
("Connect via relay", "റിലേ വഴി കണക്ട് ചെയ്യുക"),
("Always connect via relay", "എപ്പോഴും റിലേ വഴി കണക്ട് ചെയ്യുക"),
("whitelist_tip", "വൈറ്റ്‌ലിസ്റ്റ് ചെയ്ത ഐപികൾക്ക് മാത്രമേ എന്നെ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ"),
("Login", "ലോഗിൻ"),
("Verify", "പരിശോധിക്കുക"),
("Remember me", "എന്നെ ഓർമ്മിക്കുക"),
("Trust this device", "ഈ ഉപകരണം വിശ്വസിക്കുക"),
("Verification code", "വെരിഫിക്കേഷൻ കോഡ്"),
("verification_tip", "വെരിഫിക്കേഷൻ കോഡ് നിങ്ങളുടെ ഇമെയിലിലേക്ക് അയച്ചു"),
("Logout", "ലോഗൗട്ട്"),
("Tags", "ടാഗുകൾ"),
("Search ID", "ഐഡി തിരയുക"),
("whitelist_sep", "കോമ, സെമി കോളൻ അല്ലെങ്കിൽ സ്പേസ് ഉപയോഗിച്ച് തിരിക്കുക"),
("Add ID", "ഐഡി ചേർക്കുക"),
("Add Tag", "ടാഗ് ചേർക്കുക"),
("Unselect all tags", "എല്ലാ ടാഗുകളും ഒഴിവാക്കുക"),
("Network error", "നെറ്റ്‌വർക്ക് പിശക്"),
("Username missed", "യൂസർ നെയിം നൽകിയില്ല"),
("Password missed", "പാസ്‌വേഡ് നൽകിയില്ല"),
("Wrong credentials", "തെറ്റായ വിവരങ്ങൾ"),
("The verification code is incorrect or has expired", "കോഡ് തെറ്റാണ് അല്ലെങ്കിൽ കാലാവധി കഴിഞ്ഞു"),
("Edit Tag", "ടാഗ് മാറ്റുക"),
("Forget Password", "പാസ്‌വേഡ് മറന്നു"),
("Favorites", "പ്രിയപ്പെട്ടവ"),
("Add to Favorites", "പ്രിയപ്പെട്ടവയിലേക്ക് ചേർക്കുക"),
("Remove from Favorites", "പ്രിയപ്പെട്ടവയിൽ നിന്ന് നീക്കം ചെയ്യുക"),
("Empty", "ശൂന്യം"),
("Invalid folder name", "അസാധുവായ ഫോൾഡർ പേര്"),
("Socks5 Proxy", "Socks5 പ്രോക്സി"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) പ്രോക്സി"),
("Discovered", "കണ്ടെത്തിയവ"),
("install_daemon_tip", "കമ്പ്യൂട്ടർ തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കാൻ സർവീസ് ഇൻസ്റ്റാൾ ചെയ്യുക"),
("Remote ID", "റിമോട്ട് ഐഡി"),
("Paste", "പേസ്റ്റ്"),
("Paste here?", "ഇവിടെ പേസ്റ്റ് ചെയ്യണോ?"),
("Are you sure to close the connection?", "കണക്ഷൻ നിർത്തണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Download new version", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുക"),
("Touch mode", "ടച്ച് മോഡ്"),
("Mouse mode", "മൗസ് മോഡ്"),
("One-Finger Tap", "ഒരു വിരൽ ടാപ്പ്"),
("Left Mouse", "മൗസ് ഇടത് ബട്ടൺ"),
("One-Long Tap", "ഒരു നീണ്ട ടാപ്പ്"),
("Two-Finger Tap", "രണ്ട് വിരൽ ടാപ്പ്"),
("Right Mouse", "മൗസ് വലത് ബട്ടൺ"),
("One-Finger Move", "ഒരു വിരൽ നീക്കം"),
("Double Tap & Move", "രണ്ട് ടാപ്പും നീക്കവും"),
("Mouse Drag", "മൗസ് ഡ്രാഗ്"),
("Three-Finger vertically", "മൂന്ന് വിരൽ ലംബമായി"),
("Mouse Wheel", "മൗസ് വീൽ"),
("Two-Finger Move", "രണ്ട് വിരൽ നീക്കം"),
("Canvas Move", "ക്യാൻവാസ് നീക്കുക"),
("Pinch to Zoom", "സൂം ചെയ്യാൻ പിഞ്ച് ചെയ്യുക"),
("Canvas Zoom", "ക്യാൻവാസ് സൂം"),
("Reset canvas", "ക്യാൻവാസ് റീസെറ്റ് ചെയ്യുക"),
("No permission of file transfer", "ഫയൽ കൈമാറ്റത്തിന് അനുമതിയില്ല"),
("Note", "കുറിപ്പ്"),
("Connection", "കണക്ഷൻ"),
("Share screen", "സ്ക്രീൻ പങ്കിടുക"),
("Chat", "ചാറ്റ്"),
("Total", "ആകെ"),
("items", "ഇനങ്ങൾ"),
("Selected", "തിഞ്ഞെടുത്തവ"),
("Screen Capture", "സ്ക്രീൻ ക്യാപ്ചർ"),
("Input Control", "ഇൻപുട്ട് നിയന്ത്രണം"),
("Audio Capture", "ഓഡിയോ ക്യാപ്ചർ"),
("Do you accept?", "നിങ്ങൾ അംഗീകരിക്കുന്നുണ്ടോ?"),
("Open System Setting", "സിസ്റ്റം സെറ്റിംഗ്സ് തുറക്കുക"),
("How to get Android input permission?", "ആൻഡ്രോയിഡ് ഇൻപുട്ട് അനുമതി എങ്ങനെ നേടാം?"),
("android_input_permission_tip1", "ഇൻപുട്ട് അനുമതിക്കായി ആക്‌സസിബിലിറ്റി സർവീസ് ഓൺ ചെയ്യുക."),
("android_input_permission_tip2", "സെറ്റിംഗ്സിൽ RustDesk കണ്ടെത്തി അത് ഓൺ ചെയ്യുക."),
("android_new_connection_tip", "പുതിയ കണക്ഷൻ അഭ്യർത്ഥന ലഭിച്ചു."),
("android_service_will_start_tip", "സ്ക്രീൻ ക്യാപ്ചർ ഓൺ ചെയ്താൽ സർവീസ് താനേ തുടങ്ങും."),
("android_stop_service_tip", "സർവീസ് നിർത്തുന്നത് എല്ലാ കണക്ഷനുകളും വിച്ഛേദിക്കും."),
("android_version_audio_tip", "ആൻഡ്രോയിഡ് 10-ൽ കൂടുതൽ വേണം ഓഡിയോ ക്യാപ്ചർ ചെയ്യാൻ."),
("android_start_service_tip", "സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങാൻ ക്ലിക്ക് ചെയ്യുക."),
("android_permission_may_not_change_tip", "അനുമതികൾ പിന്നീട് മാറ്റാൻ കഴിയില്ല, ശ്രദ്ധിച്ച് തിരഞ്ഞെടുക്കുക."),
("Account", "അക്കൗണ്ട്"),
("Overwrite", "തിരുത്തിയെഴുതുക (Overwrite)"),
("This file exists, skip or overwrite this file?", "ഈ ഫയൽ നിലവിലുണ്ട്, ഒഴിവാക്കണോ അതോ തിരുത്തിയെഴുതണോ?"),
("Quit", "പുറത്തുകടക്കുക"),
("Help", "സഹായം"),
("Failed", "പരാജയപ്പെട്ടു"),
("Succeeded", "വിജയിച്ചു"),
("Someone turns on privacy mode, exit", "ആരോ പ്രൈവസി മോഡ് ഓൺ ചെയ്തു, പുറത്തുകടക്കുന്നു"),
("Unsupported", "പിന്തുണയ്ക്കുന്നില്ല"),
("Peer denied", "മറുഭാഗത്തുനിന്ന് നിരസിച്ചു"),
("Please install plugins", "ദയവായി പ്ലഗിനുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക"),
("Peer exit", "മറുഭാഗത്തുനിന്ന് പുറത്തുകടന്നു"),
("Failed to turn off", "ഓഫ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു"),
("Turned off", "ഓഫ് ചെയ്തു"),
("Language", "ഭാഷ"),
("Keep RustDesk background service", "RustDesk ബാക്ക്ഗ്രൗണ്ടിൽ പ്രവർത്തിപ്പിക്കുക"),
("Ignore Battery Optimizations", "ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ അവഗണിക്കുക"),
("android_open_battery_optimizations_tip", "കണക്ഷൻ മുറിയാതിരിക്കാൻ ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ സെറ്റിംഗ്സ് തുറക്കുക"),
("Start on boot", "തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കുക"),
("Start the screen sharing service on boot, requires special permissions", "തുടങ്ങുമ്പോൾ തന്നെ സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങുക, പ്രത്യേക അനുമതി ആവശ്യമാണ്"),
("Connection not allowed", "കണക്ഷൻ അനുവദനീയമല്ല"),
("Legacy mode", "ലെഗസി മോഡ്"),
("Map mode", "മാപ്പ് മോഡ്"),
("Translate mode", "ട്രാൻസ്ലേറ്റ് മോഡ്"),
("Use permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് ഉപയോഗിക്കുക"),
("Use both passwords", "രണ്ട് പാസ്‌വേഡുകളും ഉപയോഗിക്കുക"),
("Set permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് സജ്ജമാക്കുക"),
("Enable remote restart", "റിമോട്ട് റീസ്റ്റാർട്ട് അനുവദിക്കുക"),
("Restart remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുക"),
("Are you sure you want to restart", "റീസ്റ്റാർട്ട് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Restarting remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു"),
("remote_restarting_tip", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു, ദയവായി കാത്തിരിക്കുക..."),
("Copied", "കോപ്പി ചെയ്തു"),
("Exit Fullscreen", "ഫുൾ സ്ക്രീനിൽ നിന്ന് പുറത്തുകടക്കുക"),
("Fullscreen", "ഫുൾ സ്ക്രീൻ"),
("Mobile Actions", "മൊബൈൽ നടപടികൾ"),
("Select Monitor", "മോണിറ്റർ തിരഞ്ഞെടുക്കുക"),
("Control Actions", "നിയന്ത്രണ നടപടികൾ"),
("Display Settings", "ഡിസ്‌പ്ലേ ക്രമീകരണങ്ങൾ"),
("Ratio", "അനുപാതം (Ratio)"),
("Image Quality", "ചിത്രത്തിന്റെ ഗുണനിലവാരം"),
("Scroll Style", "സ്ക്രോൾ സ്റ്റൈൽ"),
("Show Toolbar", "ടൂൾബാർ കാണിക്കുക"),
("Hide Toolbar", "ടൂൾബാർ മറയ്ക്കുക"),
("Direct Connection", "നേരിട്ടുള്ള കണക്ഷൻ"),
("Relay Connection", "റിലേ കണക്ഷൻ"),
("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"),
("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"),
("Scale original", "ഒറിജിനൽ വലിപ്പം"),
("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"),
("General", "പൊതുവായവ"),
("Security", "സുരക്ഷ"),
("Theme", "തീം"),
("Dark Theme", "ഡാർക്ക് തീം"),
("Light Theme", "ലൈറ്റ് തീം"),
("Dark", "ഡാർക്ക്"),
("Light", "ലൈറ്റ്"),
("Follow System", "സിസ്റ്റം അനുസരിച്ച്"),
("Enable hardware codec", "ഹാർഡ്‌വെയർ കോഡെക് അനുവദിക്കുക"),
("Unlock Security Settings", "സുരക്ഷാ ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"),
("Enable audio", "ശബ്ദം അനുവദിക്കുക"),
("Unlock Network Settings", "നെറ്റ്‌വർക്ക് ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"),
("Server", "സെർവർ"),
("Direct IP Access", "നേരിട്ടുള്ള IP ആക്‌സസ്"),
("Proxy", "പ്രോക്സി"),
("Apply", "പ്രയോഗിക്കുക"),
("Disconnect all devices?", "എല്ലാ ഉപകരണങ്ങളും വിച്ഛേദിക്കണോ?"),
("Clear", "വൃത്തിയാക്കുക"),
("Audio Input Device", "ശബ്ദ ഇൻപുട്ട് ഉപകരണം"),
("Use IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ് ഉപയോഗിക്കുക"),
("Network", "നെറ്റ്‌വർക്ക്"),
("Pin Toolbar", "ടൂൾബാർ പിൻ ചെയ്യുക"),
("Unpin Toolbar", "ടൂൾബാർ അൺപിൻ ചെയ്യുക"),
("Recording", "റെക്കോർഡിംഗ്"),
("Directory", "ഡയറക്ടറി"),
("Automatically record incoming sessions", "വരുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"),
("Automatically record outgoing sessions", "പോകുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"),
("Change", "മാറ്റുക"),
("Start session recording", "റെക്കോർഡിംഗ് തുടങ്ങുക"),
("Stop session recording", "റെക്കോർഡിംഗ് നിർത്തുക"),
("Enable recording session", "സെഷൻ റെക്കോർഡിംഗ് അനുവദിക്കുക"),
("Enable LAN discovery", "LAN കണ്ടെത്തൽ അനുവദിക്കുക"),
("Deny LAN discovery", "LAN കണ്ടെത്തൽ നിരസിക്കുക"),
("Write a message", "സന്ദേശം എഴുതുക"),
("Prompt", "പ്രോംപ്റ്റ്"),
("Please wait for confirmation of UAC...", "UAC സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുക..."),
("elevated_foreground_window_tip", "റിമോട്ടിലെ വിൻഡോയ്ക്ക് കൂടുതൽ അനുമതി ആവശ്യമാണ്."),
("Disconnected", "വിച്ഛേദിച്ചു"),
("Other", "മറ്റുള്ളവ"),
("Confirm before closing multiple tabs", "ടാബുകൾ അടയ്ക്കുന്നതിന് മുൻപ് സ്ഥിരീകരിക്കുക"),
("Keyboard Settings", "കീബോർഡ് ക്രമീകരണങ്ങൾ"),
("Full Access", "പൂർണ്ണ ആക്‌സസ്"),
("Screen Share", "സ്ക്രീൻ ഷെയർ"),
("ubuntu-21-04-required", "Ubuntu 21.04 എങ്കിലും വേണം"),
("wayland-requires-higher-linux-version", "Wayland-ന് പുതിയ ലിനക്സ് പതിപ്പ് ആവശ്യമാണ്"),
("xdp-portal-unavailable", "XDP പോർട്ടൽ ലഭ്യമല്ല"),
("JumpLink", "ജമ്പ്‌ലിങ്ക്"),
("Please Select the screen to be shared(Operate on the peer side).", "പങ്കിടാനുള്ള സ്ക്രീൻ തിരഞ്ഞെടുക്കുക (മറുഭാഗത്ത് ചെയ്യുക)."),
("Show RustDesk", "RustDesk കാണിക്കുക"),
("This PC", "ഈ പിസി"),
("or", "അല്ലെങ്കിൽ"),
("Elevate", "എലിവേറ്റ് ചെയ്യുക"),
("Zoom cursor", "സൂം കർസർ"),
("Accept sessions via password", "പാസ്‌വേഡ് വഴി സെഷനുകൾ അനുവദിക്കുക"),
("Accept sessions via click", "ക്ലിക്ക് വഴി സെഷനുകൾ അനുവദിക്കുക"),
("Accept sessions via both", "രണ്ടും വഴി സെഷനുകൾ അനുവദിക്കുക"),
("Please wait for the remote side to accept your session request...", "മറുഭാഗം അനുമതി നൽകാനായി കാത്തിരിക്കുക..."),
("One-time Password", "ഒറ്റത്തവണ പാസ്‌വേഡ്"),
("Use one-time password", "ഒറ്റത്തവണ പാസ്‌വേഡ് ഉപയോഗിക്കുക"),
("One-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം"),
("Request access to your device", "നിങ്ങളുടെ ഉപകരണം ആക്‌സസ് ചെയ്യാൻ അനുമതി ചോദിക്കുന്നു"),
("Hide connection management window", "കണക്ഷൻ മാനേജ്‌മെന്റ് വിൻഡോ മറയ്ക്കുക"),
("hide_cm_tip", "പാസ്‌വേഡ് വഴിയുള്ള കണക്ഷൻ ആണെങ്കിൽ മാത്രം മറയ്ക്കുക"),
("wayland_experiment_tip", "Wayland പിന്തുണ പരീക്ഷണാടിസ്ഥാനത്തിലാണ്"),
("Right click to select tabs", "ടാബുകൾ തിരഞ്ഞെടുക്കാൻ വലത് ക്ലിക്ക് ചെയ്യുക"),
("Skipped", "ഒഴിവാക്കി"),
("Add to address book", "അഡ്രസ് ബുക്കിലേക്ക് ചേർക്കുക"),
("Group", "ഗ്രൂപ്പ്"),
("Search", "തിരയുക"),
("Closed manually by web console", "വെബ് കൺസോൾ വഴി മാനുവലായി അടച്ചു"),
("Local keyboard type", "ലോക്കൽ കീബോർഡ് തരം"),
("Select local keyboard type", "ലോക്കൽ കീബോർഡ് തരം തിരഞ്ഞെടുക്കുക"),
("software_render_tip", "സ്ക്രീൻ കറുത്തിരിക്കുകയാണെങ്കിൽ ഇത് പരീക്ഷിക്കുക"),
("Always use software rendering", "എപ്പോഴും സോഫ്റ്റ്‌വെയർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("config_input", "ഇൻപുട്ട് ക്രമീകരിക്കുക"),
("config_microphone", "മൈക്രോഫോൺ ക്രമീകരിക്കുക"),
("request_elevation_tip", "മറുഭാഗത്തുനിന്ന് എലവേഷൻ ആവശ്യപ്പെടുക"),
("Wait", "കാത്തിരിക്കുക"),
("Elevation Error", "എലവേഷൻ പിശക്"),
("Ask the remote user for authentication", "റിമോട്ട് ഉപയോക്താവിനോട് അനുമതി ചോദിക്കുക"),
("Choose this if the remote account is administrator", "റിമോട്ട് അക്കൗണ്ട് അഡ്മിനിസ്ട്രേറ്റർ ആണെങ്കിൽ ഇത് തിരഞ്ഞെടുക്കുക"),
("Transmit the username and password of administrator", "അഡ്മിനിസ്ട്രേറ്റർ വിവരങ്ങൾ അയക്കുക"),
("still_click_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC വിൻഡോയിൽ 'അതെ' എന്ന് ക്ലിക്ക് ചെയ്യേണ്ടതുണ്ട്."),
("Request Elevation", "എലവേഷൻ ആവശ്യപ്പെടുക"),
("wait_accept_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC അംഗീകരിക്കാൻ കാത്തിരിക്കുക."),
("Elevate successfully", "വിജയകരമായി എലവേറ്റ് ചെയ്തു"),
("uppercase", "വലിയ അക്ഷരം (Uppercase)"),
("lowercase", "ചെറിയ അക്ഷരം (Lowercase)"),
("digit", "അക്കം"),
("special character", "പ്രത്യേക ചിഹ്നം"),
("length>=8", "നീളം >= 8"),
("Weak", "ദുർബലം"),
("Medium", "ഇടത്തരം"),
("Strong", "ശക്തം"),
("Switch Sides", "വശങ്ങൾ മാറ്റുക"),
("Please confirm if you want to share your desktop?", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് പങ്കിടണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"),
("Display", "ഡിസ്‌പ്ലേ"),
("Default View Style", "സാധാരണ വ്യൂ സ്റ്റൈൽ"),
("Default Scroll Style", "സാധാരണ സ്ക്രോൾ സ്റ്റൈൽ"),
("Default Image Quality", "സാധാരണ ഇമേജ് ക്വാളിറ്റി"),
("Default Codec", "സാധാരണ കോഡെക്"),
("Bitrate", "ബിറ്റ്റേറ്റ്"),
("FPS", "FPS"),
("Auto", "ഓട്ടോ"),
("Other Default Options", "മറ്റ് സാധാരണ ഓപ്ഷനുകൾ"),
("Voice call", "വോയിസ് കോൾ"),
("Text chat", "ടെക്സ്റ്റ് ചാറ്റ്"),
("Stop voice call", "വോയിസ് കോൾ നിർത്തുക"),
("relay_hint_tip", "നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ല; റിലേ വഴി ശ്രമിക്കാം."),
("Reconnect", "വീണ്ടും കണക്ട് ചെയ്യുക"),
("Codec", "കോഡെക്"),
("Resolution", "റെസല്യൂഷൻ"),
("No transfers in progress", "കൈമാറ്റങ്ങളൊന്നും നടക്കുന്നില്ല"),
("Set one-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം നിശ്ചയിക്കുക"),
("RDP Settings", "RDP ക്രമീകരണങ്ങൾ"),
("Sort by", "ക്രമീകരിക്കുക"),
("New Connection", "പുതിയ കണക്ഷൻ"),
("Restore", "പുനഃസ്ഥാപിക്കുക"),
("Minimize", "ചുരുക്കുക"),
("Maximize", "വലുതാക്കുക"),
("Your Device", "നിങ്ങളുടെ ഉപകരണം"),
("empty_recent_tip", "സമീപകാല സെഷനുകൾ ഇവിടെ കാണാം."),
("empty_favorite_tip", "പ്രിയപ്പെട്ടവ ഇവിടെ കാണാം."),
("empty_lan_tip", "ലോക്കൽ നെറ്റ്‌വർക്കിലെ ഉപകരണങ്ങൾ ഇവിടെ കാണാം."),
("empty_address_book_tip", "അഡ്രസ് ബുക്ക് ശൂന്യമാണ്."),
("Empty Username", "യൂസർ നെയിം നൽകിയില്ല"),
("Empty Password", "പാസ്‌വേഡ് നൽകിയില്ല"),
("Me", "ഞാൻ"),
("identical_file_tip", "ഈ ഫയൽ നിലവിലുണ്ട്."),
("show_monitors_tip", "ടൂൾബാറിൽ മോണിറ്ററുകൾ കാണിക്കുക"),
("View Mode", "വ്യൂ മോഡ്"),
("login_linux_tip", "റിമോട്ട് ലിനക്സ് സെഷനായി ലോഗിൻ ചെയ്യണം"),
("verify_rustdesk_password_tip", "RustDesk പാസ്‌വേഡ് പരിശോധിക്കുക"),
("remember_account_tip", "ഈ അക്കൗണ്ട് ഓർമ്മിക്കുക"),
("os_account_desk_tip", "ആക്‌സസിനായി OS അക്കൗണ്ട് ഉപയോഗിക്കുക"),
("OS Account", "OS അക്കൗണ്ട്"),
("another_user_login_title_tip", "മറ്റൊരു ഉപയോക്താവ് ലോഗിൻ ചെയ്തിട്ടുണ്ട്"),
("another_user_login_text_tip", "വിച്ഛേദിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക"),
("xorg_not_found_title_tip", "Xorg കണ്ടെത്താനായില്ല"),
("xorg_not_found_text_tip", "ദയവായി Xorg ഇൻസ്റ്റാൾ ചെയ്യുക"),
("no_desktop_title_tip", "ഡെസ്ക്ടോപ്പ് ലഭ്യമല്ല"),
("no_desktop_text_tip", "ദയവായി ലിനക്സ് ഡെസ്ക്ടോപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക"),
("No need to elevate", "എലവേറ്റ് ചെയ്യേണ്ടതില്ല"),
("System Sound", "സിസ്റ്റം സൗണ്ട്"),
("Default", "ഡിഫോൾട്ട്"),
("New RDP", "പുതിയ RDP"),
("Fingerprint", "ഫിംഗർപ്രിന്റ്"),
("Copy Fingerprint", "ഫിംഗർപ്രിന്റ് കോപ്പി ചെയ്യുക"),
("no fingerprints", "ഫിംഗർപ്രിന്റുകൾ ഇല്ല"),
("Select a peer", "ഒരാളെ തിരഞ്ഞെടുക്കുക"),
("Select peers", "തിരഞ്ഞെടുക്കുക"),
("Plugins", "പ്ലഗിനുകൾ"),
("Uninstall", "അൺഇൻസ്റ്റാൾ ചെയ്യുക"),
("Update", "അപ്ഡേറ്റ് ചെയ്യുക"),
("Enable", "പ്രവർത്തനക്ഷമമാക്കുക"),
("Disable", "പ്രവർത്തനരഹിതമാക്കുക"),
("Options", "ഓപ്ഷനുകൾ"),
("resolution_original_tip", "ഒറിജിനൽ റെസല്യൂഷൻ"),
("resolution_fit_local_tip", "ലോക്കൽ സ്ക്രീനിന് അനുയോജ്യം"),
("resolution_custom_tip", "കസ്റ്റം റെസല്യൂഷൻ"),
("Collapse toolbar", "ടൂൾബാർ ചുരുക്കുക"),
("Accept and Elevate", "അംഗീകരിച്ച് എലവേറ്റ് ചെയ്യുക"),
("accept_and_elevate_btn_tooltip", "കണക്ഷൻ അംഗീകരിച്ച് UAC അനുമതികൾ നൽകുക."),
("clipboard_wait_response_timeout_tip", "ക്ലിപ്പ്ബോർഡ് മറുപടിക്കായി കാത്തിരുന്നു സമയം കഴിഞ്ഞു."),
("Incoming connection", "വരുന്ന കണക്ഷൻ"),
("Outgoing connection", "പോകുന്ന കണക്ഷൻ"),
("Exit", "പുറത്തുകടക്കുക"),
("Open", "തുറക്കുക"),
("logout_tip", "നിങ്ങൾക്ക് ലോഗൗട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?"),
("Service", "സർവീസ്"),
("Start", "തുടങ്ങുക"),
("Stop", "നിർത്തുക"),
("exceed_max_devices", "നിങ്ങൾ ഉപകരണങ്ങളുടെ പരിധി കവിഞ്ഞു."),
("Sync with recent sessions", "സമീപകാല സെഷനുകളുമായി സિંക് ചെയ്യുക"),
("Sort tags", "ടാഗുകൾ ക്രമീകരിക്കുക"),
("Open connection in new tab", "പുതിയ ടാബിൽ തുറക്കുക"),
("Move tab to new window", "ടാബ് പുതിയ വിൻഡോയിലേക്ക് മാറ്റുക"),
("Can not be empty", "ശൂന്യമാകാൻ പാടില്ല"),
("Already exists", "നിലവിലുണ്ട്"),
("Change Password", "പാസ്‌വേഡ് മാറ്റുക"),
("Refresh Password", "പാസ്‌വേഡ് പുതുക്കുക"),
("ID", "ഐഡി"),
("Grid View", "ഗ്രിഡ് വ്യൂ"),
("List View", "ലിസ്റ്റ് വ്യൂ"),
("Select", "തിരഞ്ഞെടുക്കുക"),
("Toggle Tags", "ടാഗുകൾ മാറ്റുക"),
("pull_ab_failed_tip", "അഡ്രസ് ബുക്ക് അപ്‌ഡേറ്റ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."),
("push_ab_failed_tip", "അഡ്രസ് ബുക്ക് സિંക് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."),
("synced_peer_readded_tip", "സമീപകാല ഉപകരണം അഡ്രസ് ബുക്കിലേക്ക് സિંക് ചെയ്തു."),
("Change Color", "നിറം മാറ്റുക"),
("Primary Color", "പ്രധാന നിറം"),
("HSV Color", "HSV നിറം"),
("Installation Successful!", "ഇൻസ്റ്റാളേഷൻ വിജയിച്ചു!"),
("Installation failed!", "ഇൻസ്റ്റാളേഷൻ പരാജയപ്പെട്ടു!"),
("Reverse mouse wheel", "മൗസ് വീൽ തിരിക്കുക"),
("{} sessions", "{} സെഷനുകൾ"),
("scam_title", "തട്ടിപ്പ് മുന്നറിയിപ്പ്!"),
("scam_text1", "നിങ്ങൾക്ക് പരിചയമില്ലാത്ത ആരെങ്കിലും RustDesk ഉപയോഗിക്കാൻ ആവശ്യപ്പെട്ടാൽ ഉടൻ കണക്ഷൻ വിച്ഛേദിക്കുക."),
("scam_text2", "ഇതൊരു തട്ടിപ്പായിരിക്കാം. ആർക്കും പാസ്‌വേഡ് നൽകരുത്."),
("Don't show again", "വീണ്ടും കാണിക്കരുത്"),
("I Agree", "ഞാൻ സമ്മതിക്കുന്നു"),
("Decline", "നിരസിക്കുന്നു"),
("Timeout in minutes", "മിനിറ്റുകളിൽ സമയം നിശ്ചയിക്കുക"),
("auto_disconnect_option_tip", "പ്രവർത്തനമില്ലെങ്കിൽ താനേ വിച്ഛേദിക്കുക"),
("Connection failed due to inactivity", "പ്രവർത്തനമില്ലാത്തതിനാൽ കണക്ഷൻ വിച്ഛേദിച്ചു"),
("Check for software update on startup", "തുടങ്ങുമ്പോൾ അപ്‌ഡേറ്റ് ഉണ്ടോ എന്ന് പരിശോധിക്കുക"),
("upgrade_rustdesk_server_pro_to_{}_tip", "സെർവർ പ്രോ {} ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക"),
("pull_group_failed_tip", "ഗ്രൂപ്പ് വിവരങ്ങൾ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു"),
("Filter by intersection", "ഇന്റർസെക്ഷൻ വഴി ഫിൽട്ടർ ചെയ്യുക"),
("Remove wallpaper during incoming sessions", "കണക്ഷൻ സമയത്ത് വാൾപേപ്പർ മാറ്റുക"),
("Test", "പരിശോധിക്കുക"),
("display_is_plugged_out_msg", "ഡിസ്‌പ്ലേ ഊരിയിരിക്കുകയാണ്."),
("No displays", "ഡിസ്‌പ്ലേകൾ ഇല്ല"),
("Open in new window", "പുതിയ വിൻഡോയിൽ തുറക്കുക"),
("Show displays as individual windows", "ഓരോ ഡിസ്‌പ്ലേയും ഓരോ വിൻഡോയായി കാണിക്കുക"),
("Use all my displays for the remote session", "എല്ലാ ഡിസ്‌പ്ലേകളും ഉപയോഗിക്കുക"),
("selinux_tip", "SELinux പ്രവർത്തനക്ഷമമാണ്."),
("Change view", "കാഴ്ച മാറ്റുക"),
("Big tiles", "വലിയ ടൈലുകൾ"),
("Small tiles", "ചെറിയ ടൈലുകൾ"),
("List", "ലിസ്റ്റ്"),
("Virtual display", "വെർച്വൽ ഡിസ്‌പ്ലേ"),
("Plug out all", "എല്ലാം ഊരുക"),
("True color (4:4:4)", "ട്രൂ കളർ (4:4:4)"),
("Enable blocking user input", "യൂസർ ഇൻപുട്ട് തടയുന്നത് അനുവദിക്കുക"),
("id_input_tip", "നിങ്ങൾക്ക് ഐഡി, ഏലിയാസ് അല്ലെങ്കിൽ ഐപി നൽകാം."),
("privacy_mode_impl_mag_tip", "മാഗ്നിഫയർ സ്വകാര്യ മോഡ്"),
("privacy_mode_impl_virtual_display_tip", "വെർച്വൽ ഡിസ്‌പ്ലേ സ്വകാര്യ മോഡ്"),
("Enter privacy mode", "സ്വകാര്യ മോഡിലേക്ക് കടക്കുക"),
("Exit privacy mode", "സ്വകാര്യ മോഡിൽ നിന്ന് പുറത്തുകടക്കുക"),
("idd_not_support_under_win10_2004_tip", "Windows 10 (2004) എങ്കിലും വേണം."),
("input_source_1_tip", "ഇൻപുട്ട് സോഴ്സ് 1"),
("input_source_2_tip", "ഇൻപുട്ട് സോഴ്സ് 2"),
("Swap control-command key", "Control-Command കീകൾ പരസ്പരം മാറ്റുക"),
("swap-left-right-mouse", "ഇടത്-വലത് മൗസ് ബട്ടണുകൾ മാറ്റുക"),
("2FA code", "2FA കോഡ്"),
("More", "കൂടുതൽ"),
("enable-2fa-title", "2FA ഓൺ ചെയ്യുക"),
("enable-2fa-desc", "അതന്റിക്കേറ്റർ ആപ്പ് സജ്ജമാക്കുക."),
("wrong-2fa-code", "തെറ്റായ 2FA കോഡ്."),
("enter-2fa-title", "2FA കോഡ് നൽകുക"),
("Email verification code must be 6 characters.", "ഇമെയിൽ കോഡ് 6 അക്ഷരങ്ങൾ വേണം."),
("2FA code must be 6 digits.", "2FA കോഡ് 6 അക്കങ്ങൾ വേണം."),
("Multiple Windows sessions found", "ഒന്നിലധികം വിൻഡോസ് സെഷനുകൾ കണ്ടെത്തി"),
("Please select the session you want to connect to", "ബന്ധിപ്പിക്കേണ്ട സെഷൻ തിരഞ്ഞെടുക്കുക"),
("powered_by_me", "ഞാൻ നിർമ്മിച്ചത്"),
("outgoing_only_desk_tip", "ഇതൊരു ഔട്ട്‌ഗോയിംഗ് മോഡ് മാത്രമാണ്"),
("preset_password_warning", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മാറ്റുക."),
("Security Alert", "സുരക്ഷാ മുന്നറിയിപ്പ്"),
("My address book", "എന്റെ അഡ്രസ് ബുക്ക്"),
("Personal", "വ്യക്തിഗതം"),
("Owner", "ഉടമസ്ഥൻ"),
("Set shared password", "പങ്കിട്ട പാസ്‌വേഡ് സജ്ജമാക്കുക"),
("Exist in", "നിലവിലുള്ളത്"),
("Read-only", "വായിക്കാൻ മാത്രം"),
("Read/Write", "വായിക്കാനും എഴുതാനും"),
("Full Control", "പൂർണ്ണ നിയന്ത്രണം"),
("share_warning_tip", "നിങ്ങളുടെ വിവരങ്ങൾ പങ്കിടുകയാണ്."),
("Everyone", "എല്ലാവരും"),
("ab_web_console_tip", "വെബ് കൺസോൾ അഡ്രസ് ബുക്ക്"),
("allow-only-conn-window-open-tip", "RustDesk വിൻഡോ തുറന്നിരിക്കുമ്പോൾ മാത്രം കണക്ഷൻ അനുവദിക്കുക"),
("no_need_privacy_mode_no_physical_displays_tip", "ഡിസ്‌പ്ലേ ഇല്ലാത്തതിനാൽ സ്വകാര്യ മോഡ് ആവശ്യമില്ല."),
("Follow remote cursor", "റിമോട്ട് കർസറിനെ പിന്തുടരുക"),
("Follow remote window focus", "റിമോട്ട് വിൻഡോ ഫോക്കസിനെ പിന്തുടരുക"),
("default_proxy_tip", "ഡിഫോൾട്ട് പ്രോക്സി ക്രമീകരണം"),
("no_audio_input_device_tip", "ഓഡിയോ ഇൻപുട്ട് ഉപകരണം കണ്ടെത്തിയില്ല."),
("Incoming", "വരുന്നവ"),
("Outgoing", "പോകുന്നവ"),
("Clear Wayland screen selection", "Wayland സ്ക്രീൻ സെലക്ഷൻ മാറ്റുക"),
("clear_Wayland_screen_selection_tip", "സ്ക്രീൻ സെലക്ഷൻ റീസെറ്റ് ചെയ്യുക."),
("confirm_clear_Wayland_screen_selection_tip", "സെലക്ഷൻ മാറ്റണമെന്ന് ഉറപ്പാണോ?"),
("android_new_voice_call_tip", "പുതിയ വോയിസ് കോൾ അഭ്യർത്ഥന"),
("texture_render_tip", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Use texture rendering", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Floating window", "ഫ്ലോട്ടിംഗ് വിൻഡോ"),
("floating_window_tip", "ബാക്ക്ഗ്രൗണ്ടിലാണെങ്കിലും RustDesk കാണിക്കുക"),
("Keep screen on", "സ്ക്രീൻ ഓഫ് ആകാതെ വെക്കുക"),
("Never", "ഒരിക്കലുമില്ല"),
("During controlled", "നിയന്ത്രിക്കുമ്പോൾ"),
("During service is on", "സർവീസ് ഓൺ ആയിരിക്കുമ്പോൾ"),
("Capture screen using DirectX", "DirectX ഉപയോഗിച്ച് സ്ക്രീൻ ക്യാപ്ചർ ചെയ്യുക"),
("Back", "പുറകോട്ട്"),
("Apps", "ആപ്പുകൾ"),
("Volume up", "ശബ്ദം കൂട്ടുക"),
("Volume down", "ശബ്ദം കുറയ്ക്കുക"),
("Power", "പവർ"),
("Telegram bot", "ടെലഗ്രാം ബോട്ട്"),
("enable-bot-tip", "അറിയിപ്പുകൾക്കായി ബോട്ട് ഓൺ ചെയ്യുക"),
("enable-bot-desc", "ടെലഗ്രാം ബോട്ട് സജ്ജമാക്കുക."),
("cancel-2fa-confirm-tip", "2FA റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"),
("cancel-bot-confirm-tip", "ബോട്ട് റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"),
("About RustDesk", "RustDesk-നെ കുറിച്ച്"),
("Send clipboard keystrokes", "ക്ലിപ്പ്ബോർഡ് കീസ്ട്രോക്കുകൾ അയക്കുക"),
("network_error_tip", "നെറ്റ്‌വർക്ക് പിശക്, വീണ്ടും ശ്രമിക്കുക."),
("Unlock with PIN", "പിൻ ഉപയോഗിച്ച് അൺലോക്ക് ചെയ്യുക"),
("Requires at least {} characters", "കുറഞ്ഞത് {} അക്ഷരങ്ങൾ വേണം"),
("Wrong PIN", "തെറ്റായ പിൻ"),
("Set PIN", "പിൻ സജ്ജമാക്കുക"),
("Enable trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ അനുവദിക്കുക"),
("Manage trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ നിയന്ത്രിക്കുക"),
("Platform", "പ്ലാറ്റ്‌ഫോം"),
("Days remaining", "ബാക്കിയുള്ള ദിവസങ്ങൾ"),
("enable-trusted-devices-tip", "വിശ്വസനീയമായവയ്ക്ക് പാസ്‌വേഡ് വേണ്ട"),
("Parent directory", "പ്രധാന ഡയറക്ടറി"),
("Resume", "തുടരുക"),
("Invalid file name", "അസാധുവായ ഫയൽ പേര്"),
("one-way-file-transfer-tip", "ഒരു വശത്തേക്ക് മാത്രമുള്ള ഫയൽ കൈമാറ്റം"),
("Authentication Required", "അംഗീകാരം ആവശ്യമാണ്"),
("Authenticate", "അംഗീകരിക്കുക"),
("web_id_input_tip", "റിമോട്ട് ഐഡി നൽകുക"),
("Download", "ഡൗൺലോഡ്"),
("Upload folder", "ഫോൾഡർ അപ്‌ലോഡ് ചെയ്യുക"),
("Upload files", "ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യുക"),
("Clipboard is synchronized", "ക്ലിപ്പ്ബോർഡ് സങ്കലനം ചെയ്തു"),
("Update client clipboard", "ക്ലയന്റ് ക്ലിപ്പ്ബോർഡ് പുതുക്കുക"),
("Untagged", "ടാഗ് ചെയ്യാത്തവ"),
("new-version-of-{}-tip", "{} പുതിയ പതിപ്പ് ലഭ്യമാണ്"),
("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"),
("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"),
("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Printer", "പ്രിന്റർ"),
("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."),
("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."),
("printer-{}-not-installed-tip", "പ്രിന്റർ {} ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല."),
("printer-{}-ready-tip", "പ്രിന്റർ {} തയ്യാറാണ്."),
("Install {} Printer", "{} പ്രിന്റർ ഇൻസ്റ്റാൾ ചെയ്യുക"),
("Outgoing Print Jobs", "പോകുന്ന പ്രിന്റ് ജോലികൾ"),
("Incoming Print Jobs", "വരുന്ന പ്രിന്റ് ജോലികൾ"),
("Incoming Print Job", "വരുന്ന പ്രിന്റ് ജോലി"),
("use-the-default-printer-tip", "ഡിഫോൾട്ട് പ്രിന്റർ ഉപയോഗിക്കുക"),
("use-the-selected-printer-tip", "തിഞ്ഞെടുത്ത പ്രിന്റർ ഉപയോഗിക്കുക"),
("auto-print-tip", "താനേ പ്രിന്റ് ചെയ്യുക"),
("print-incoming-job-confirm-tip", "പ്രിന്റ് ചെയ്യുന്നതിന് മുൻപ് ചോദിക്കുക"),
("remote-printing-disallowed-tile-tip", "റിമോട്ട് പ്രിന്റിംഗ് അനുവദനീയമല്ല"),
("remote-printing-disallowed-text-tip", "സെറ്റിംഗ്സിൽ റിമോട്ട് പ്രിന്റിംഗ് ഓൺ ചെയ്യുക."),
("save-settings-tip", "സെറ്റിംഗ്സ് സേവ് ചെയ്യുക"),
("dont-show-again-tip", "വീണ്ടും കാണിക്കരുത്"),
("Take screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുക"),
("Taking screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുന്നു"),
("screenshot-merged-screen-not-supported-tip", "മെർജ് ചെയ്ത സ്ക്രീൻഷോട്ട് പിന്തുണയ്ക്കുന്നില്ല."),
("screenshot-action-tip", "സ്ക്രീൻഷോട്ടിന് ശേഷമുള്ള നടപടി"),
("Save as", "പേരിൽ സേവ് ചെയ്യുക"),
("Copy to clipboard", "ക്ലിപ്പ്ബോർഡിലേക്ക് കോപ്പി ചെയ്യുക"),
("Enable remote printer", "റിമോട്ട് പ്രിന്റർ അനുവദിക്കുക"),
("Downloading {}", "{} ഡൗൺലോഡ് ചെയ്യുന്നു"),
("{} Update", "{} അപ്‌ഡേറ്റ്"),
("{}-to-update-tip", "അപ്‌ഡേറ്റ് ചെയ്യാൻ {}"),
("download-new-version-failed-tip", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."),
("Auto update", "ഓട്ടോ അപ്‌ഡേറ്റ്"),
("update-failed-check-msi-tip", "അപ്‌ഡേറ്റ് പരാജയപ്പെട്ടു, MSI ഫയൽ പരിശോധിക്കുക."),
("websocket_tip", "പോട്ടുകൾ തടഞ്ഞിട്ടുണ്ടെങ്കിൽ WebSocket ഉപയോഗിക്കുക."),
("Use WebSocket", "WebSocket ഉപയോഗിക്കുക"),
("Trackpad speed", "ട്രാക്ക്പാഡ് വേഗത"),
("Default trackpad speed", "സാധാരണ ട്രാക്ക്പാഡ് വേഗത"),
("Numeric one-time password", "അക്കങ്ങൾ മാത്രമുള്ള OTP"),
("Enable IPv6 P2P connection", "IPv6 P2P കണക്ഷൻ അനുവദിക്കുക"),
("Enable UDP hole punching", "UDP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"),
("View camera", "ക്യാമറ കാണുക"),
("Enable camera", "ക്യാമറ ഓൺ ചെയ്യുക"),
("No cameras", "ക്യാമറകൾ കണ്ടെത്തിയില്ല"),
("view_camera_unsupported_tip", "റിമോട്ട് ക്യാമറ പിന്തുണയ്ക്കുന്നില്ല."),
("Terminal", "ടെർമിനൽ"),
("Enable terminal", "ടെർമിനൽ അനുവദിക്കുക"),
("New tab", "പുതിയ ടാബ്"),
("Keep terminal sessions on disconnect", "വിച്ഛേദിക്കുമ്പോൾ ടെർമിനൽ സെഷൻ നിർത്തരുത്"),
("Terminal (Run as administrator)", "ടെർമിനൽ (അഡ്മിനിസ്ട്രേറ്ററായി)"),
("terminal-admin-login-tip", "അഡ്മിൻ ലോഗിൻ ആവശ്യമാണ്."),
("Failed to get user token.", "യൂസർ ടോക്കൺ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു."),
("Incorrect username or password.", "തെറ്റായ യൂസർ നെയിം അല്ലെങ്കിൽ പാസ്‌വേഡ്."),
("The user is not an administrator.", "ഉപയോക്താവ് അഡ്മിനിസ്ട്രേറ്ററല്ല."),
("Failed to check if the user is an administrator.", "അഡ്മിൻ ആണോ എന്ന് പരിശോധിക്കുന്നതിൽ പരാജയപ്പെട്ടു."),
("Supported only in the installed version.", "ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പിൽ മാത്രം ലഭ്യം."),
("elevation_username_tip", "അഡ്മിനിസ്ട്രേറ്റർ പേര് നൽകുക"),
("Preparing for installation ...", "ഇൻസ്റ്റാളേഷനായി ഒരുങ്ങുന്നു..."),
("Show my cursor", "എന്റെ കർസർ കാണിക്കുക"),
("Scale custom", "കസ്റ്റം സ്കെയിൽ"),
("Custom scale slider", "കസ്റ്റം സ്കെയിൽ സ്ലൈഡർ"),
("Decrease", "കുറയ്ക്കുക"),
("Increase", "കൂട്ടുക"),
("Show virtual mouse", "വെർച്വൽ മൗസ് കാണിക്കുക"),
("Virtual mouse size", "വെർച്വൽ മൗസ് വലിപ്പം"),
("Small", "ചെറുത്"),
("Large", "വലുത്"),
("Show virtual joystick", "വെർച്വൽ ജോയ്സ്റ്റിക് കാണിക്കുക"),
("Edit note", "കുറിപ്പ് മാറ്റുക"),
("Alias", "ഏലിയാസ് (Alias)"),
("ScrollEdge", "സ്ക്രോൾ എഡ്ജ്"),
("Allow insecure TLS fallback", "സുരക്ഷിതമല്ലാത്ത TLS അനുവദിക്കുക"),
("allow-insecure-tls-fallback-tip", "പഴയ സെർവറുകൾക്കായി ഉപയോഗിക്കുക."),
("Disable UDP", "UDP ഒഴിവാക്കുക"),
("disable-udp-tip", "കണക്ഷൻ പ്രശ്നങ്ങൾക്ക് UDP ഒഴിവാക്കുക."),
("server-oss-not-support-tip", "OSS സെർവർ ഇത് പിന്തുണയ്ക്കുന്നില്ല."),
("input note here", "ഇവിടെ കുറിപ്പ് എഴുതുക"),
("note-at-conn-end-tip", "കണക്ഷൻ കഴിയുമ്പോൾ കുറിപ്പ് കാണിക്കുക"),
("Show terminal extra keys", "ടെർമിനൽ കീകൾ കാണിക്കുക"),
("Relative mouse mode", "റിലേറ്റീവ് മൗസ് മോഡ്"),
("rel-mouse-not-supported-peer-tip", "മറുഭാഗം പിന്തുണയ്ക്കുന്നില്ല."),
("rel-mouse-not-ready-tip", "തയ്യാറായിട്ടില്ല."),
("rel-mouse-lock-failed-tip", "മൗസ് ലോക്ക് പരാജയപ്പെട്ടു."),
("rel-mouse-exit-{}-tip", "പുറത്തുകടക്കാൻ {} അമർത്തുക"),
("rel-mouse-permission-lost-tip", "അനുമതി നഷ്ടപ്പെട്ടു."),
("Changelog", "മാറ്റങ്ങൾ (Changelog)"),
("keep-awake-during-outgoing-sessions-label", "സെഷൻ നടക്കുമ്പോൾ ഉറക്കത്തിലാകരുത്"),
("keep-awake-during-incoming-sessions-label", "സെഷൻ വരുമ്പോൾ ഉറക്കത്തിലാകരുത്"),
("Continue with {}", "{} ഉപയോഗിച്ച് തുടരുക"),
("Display Name", "ഡിസ്‌പ്ലേ പേര്"),
("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."),
("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
("Continue with {}", "Ga verder met {}"),
("Display Name", "Naam Weergeven"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
].iter().cloned().collect();
}

View File

@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Udostępnianie ekranu"),
("ubuntu-21-04-required", "Wayland wymaga Ubuntu 21.04 lub nowszego."),
("wayland-requires-higher-linux-version", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."),
("xdp-portal-unavailable", ""),
("xdp-portal-unavailable", "Nie udało się przechwycić ekranu Wayland. Portal XDG Desktop mógł ulec awarii lub jest niedostępny. Spróbuj go ponownie uruchomić poleceniem `systemctl --user restart xdg-desktop-portal`."),
("JumpLink", "Podgląd"),
("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."),
("Show RustDesk", "Pokaż RustDesk"),
@@ -740,8 +740,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
("Continue with {}", "Kontynuuj z {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Display Name", "Nazwa wyświetlana"),
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
].iter().cloned().collect();
}

View File

@@ -62,7 +62,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Invalid format", "Format nevalid"),
("server_not_support", "Încă nu este compatibil cu serverul"),
("Not available", "Indisponibil"),
("Too frequent", "Modificat prea frecvent"),
("Too frequent", "Prea frecvent"),
("Cancel", "Anulează"),
("Skip", "Omite"),
("Close", "Închide"),
@@ -87,7 +87,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Modified", "Modificat"),
("Size", "Dimensiune"),
("Show Hidden Files", "Afișează fișiere ascunse"),
("Receive", "Acceptă"),
("Receive", "Primește"),
("Send", "Trimite"),
("Refresh File", "Actualizează fișier"),
("Local", "Local"),
@@ -108,7 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do this for all conflicts", "Aplică la toate conflictele"),
("This is irreversible!", "Această acțiune este ireversibilă!"),
("Deleting", "În curs de ștergere..."),
("files", "fișier"),
("files", "fișiere"),
("Waiting", "În așteptare..."),
("Finished", "Finalizat"),
("Speed", "Viteză"),
@@ -203,7 +203,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("x11 expected", "Este necesar X11"),
("Port", "Port"),
("Settings", "Setări"),
("Username", " Nume utilizator"),
("Username", "Nume utilizator"),
("Invalid port", "Port nevalid"),
("Closed manually by the peer", "Conexiune închisă manual de dispozitivul pereche"),
("Enable remote configuration modification", "Activează modificarea configurației de la distanță"),
@@ -216,7 +216,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remember me", "Reține-mă"),
("Trust this device", "Acest dispozitiv este de încredere"),
("Verification code", "Cod de verificare"),
("verification_tip", ""),
("verification_tip", "Introdu codul de verificare trimis la adresa ta de e-mail sau generat de aplicația de autentificare."),
("Logout", "Deconectează-te"),
("Tags", "Etichete"),
("Search ID", "Caută după ID"),
@@ -228,9 +228,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Username missed", "Lipsește numele de utilizator"),
("Password missed", "Lipsește parola"),
("Wrong credentials", "Nume sau parolă greșită"),
("The verification code is incorrect or has expired", ""),
("The verification code is incorrect or has expired", "Codul de verificare este incorect sau a expirat"),
("Edit Tag", "Modifică etichetă"),
("Forget Password", "Uită parola"),
("Forget Password", "Parolă uitată"),
("Favorites", "Favorite"),
("Add to Favorites", "Adaugă la Favorite"),
("Remove from Favorites", "Șterge din Favorite"),
@@ -263,7 +263,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Canvas Zoom", "Mărire ecran"),
("Reset canvas", "Reinițializează ecranul"),
("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"),
("Note", "Reține"),
("Note", "Notă"),
("Connection", "Conexiune"),
("Share screen", "Partajează ecran"),
("Chat", "Mesaje"),
@@ -276,14 +276,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Accepți?"),
("Open System Setting", "Deschide setări sistem"),
("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"),
("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul „Accesibilitate."),
("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate\"."),
("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."),
("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."),
("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."),
("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."),
("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."),
("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Capturare ecran] pentru a porni serviciul de partajare a ecranului."),
("android_permission_may_not_change_tip", ""),
("android_permission_may_not_change_tip", "Este posibil ca unele permisiuni să nu poată fi modificate în funcție de versiunea de Android."),
("Account", "Cont"),
("Overwrite", "Suprascrie"),
("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"),
@@ -304,15 +304,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."),
("Start on boot", "Pornește la boot"),
("Start the screen sharing service on boot, requires special permissions", "Pornește serviciul de partajare a ecranului la boot; necesită permisiuni speciale"),
("Connection not allowed", "Conexiune neautoriztă"),
("Connection not allowed", "Conexiune neautoriza"),
("Legacy mode", "Mod legacy"),
("Map mode", "Mod hartă"),
("Translate mode", "Mod traducere"),
("Use permanent password", "Folosește parola permanentă"),
("Use both passwords", "Folosește ambele programe"),
("Use both passwords", "Folosește ambele parole"),
("Set permanent password", "Setează parola permanentă"),
("Enable remote restart", "Activează repornirea la distanță"),
("Restart remote device", "Repornește dispozivul la distanță"),
("Restart remote device", "Repornește dispozitivul la distanță"),
("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"),
("Restarting remote device", "Se repornește dispozitivul la distanță"),
("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."),
@@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unpin Toolbar", "Detașează bara de instrumente"),
("Recording", "Înregistrare"),
("Directory", "Director"),
("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"),
("Automatically record outgoing sessions", ""),
("Automatically record incoming sessions", "Înregistrează automat sesiunile primite"),
("Automatically record outgoing sessions", "Înregistrează automat sesiunile de ieșire"),
("Change", "Modifică"),
("Start session recording", "Începe înregistrarea"),
("Stop session recording", "Oprește înregistrarea"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Partajare ecran"),
("ubuntu-21-04-required", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."),
("wayland-requires-higher-linux-version", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."),
("xdp-portal-unavailable", ""),
("xdp-portal-unavailable", "Portalul XDG Desktop nu este disponibil. Asigură-te că rulezi o sesiune Wayland cu suport pentru portal."),
("JumpLink", "Afișează"),
("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."),
("Show RustDesk", "Afișează RustDesk"),
@@ -436,13 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Default Image Quality", "Calitatea implicită a imaginii"),
("Default Codec", "Codec implicit"),
("Bitrate", "Rată de biți"),
("FPS", "CPS"),
("FPS", "FPS"),
("Auto", "Auto"),
("Other Default Options", "Alte opțiuni implicite"),
("Voice call", "Apel vocal"),
("Text chat", "Conversație text"),
("Stop voice call", "Încheie apel vocal"),
("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."),
("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r\" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."),
("Reconnect", "Reconectează-te"),
("Codec", "Codec"),
("Resolution", "Rezoluție"),
@@ -503,245 +503,245 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Exit", "Ieși"),
("Open", "Deschide"),
("logout_tip", "Sigur vrei să te deconectezi?"),
("Service", ""),
("Start", ""),
("Stop", ""),
("exceed_max_devices", ""),
("Sync with recent sessions", ""),
("Sort tags", ""),
("Open connection in new tab", ""),
("Move tab to new window", ""),
("Can not be empty", ""),
("Already exists", ""),
("Change Password", ""),
("Refresh Password", ""),
("ID", ""),
("Grid View", ""),
("List View", ""),
("Select", ""),
("Toggle Tags", ""),
("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
("Installation Successful!", ""),
("Installation failed!", ""),
("Reverse mouse wheel", ""),
("{} sessions", ""),
("scam_title", ""),
("scam_text1", ""),
("scam_text2", ""),
("Don't show again", ""),
("I Agree", ""),
("Decline", ""),
("Timeout in minutes", ""),
("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""),
("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
("pull_group_failed_tip", ""),
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
("Use all my displays for the remote session", ""),
("selinux_tip", ""),
("Change view", ""),
("Big tiles", ""),
("Small tiles", ""),
("List", ""),
("Virtual display", ""),
("Plug out all", ""),
("True color (4:4:4)", ""),
("Enable blocking user input", ""),
("id_input_tip", ""),
("privacy_mode_impl_mag_tip", ""),
("privacy_mode_impl_virtual_display_tip", ""),
("Enter privacy mode", ""),
("Exit privacy mode", ""),
("idd_not_support_under_win10_2004_tip", ""),
("input_source_1_tip", ""),
("input_source_2_tip", ""),
("Swap control-command key", ""),
("swap-left-right-mouse", ""),
("2FA code", ""),
("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
("Email verification code must be 6 characters.", ""),
("2FA code must be 6 digits.", ""),
("Multiple Windows sessions found", ""),
("Please select the session you want to connect to", ""),
("powered_by_me", ""),
("outgoing_only_desk_tip", ""),
("preset_password_warning", ""),
("Security Alert", ""),
("My address book", ""),
("Personal", ""),
("Owner", ""),
("Set shared password", ""),
("Exist in", ""),
("Read-only", ""),
("Read/Write", ""),
("Full Control", ""),
("share_warning_tip", ""),
("Everyone", ""),
("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
("default_proxy_tip", ""),
("no_audio_input_device_tip", ""),
("Incoming", ""),
("Outgoing", ""),
("Clear Wayland screen selection", ""),
("clear_Wayland_screen_selection_tip", ""),
("confirm_clear_Wayland_screen_selection_tip", ""),
("android_new_voice_call_tip", ""),
("texture_render_tip", ""),
("Use texture rendering", ""),
("Floating window", ""),
("floating_window_tip", ""),
("Keep screen on", ""),
("Never", ""),
("During controlled", ""),
("During service is on", ""),
("Capture screen using DirectX", ""),
("Back", ""),
("Apps", ""),
("Volume up", ""),
("Volume down", ""),
("Power", ""),
("Telegram bot", ""),
("enable-bot-tip", ""),
("enable-bot-desc", ""),
("cancel-2fa-confirm-tip", ""),
("cancel-bot-confirm-tip", ""),
("About RustDesk", ""),
("Send clipboard keystrokes", ""),
("network_error_tip", ""),
("Unlock with PIN", ""),
("Requires at least {} characters", ""),
("Wrong PIN", ""),
("Set PIN", ""),
("Enable trusted devices", ""),
("Manage trusted devices", ""),
("Platform", ""),
("Days remaining", ""),
("enable-trusted-devices-tip", ""),
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
("web_id_input_tip", ""),
("Download", ""),
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
("new-version-of-{}-tip", ""),
("Accessible devices", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("d3d_render_tip", ""),
("Use D3D rendering", ""),
("Printer", ""),
("printer-os-requirement-tip", ""),
("printer-requires-installed-{}-client-tip", ""),
("printer-{}-not-installed-tip", ""),
("printer-{}-ready-tip", ""),
("Install {} Printer", ""),
("Outgoing Print Jobs", ""),
("Incoming Print Jobs", ""),
("Incoming Print Job", ""),
("use-the-default-printer-tip", ""),
("use-the-selected-printer-tip", ""),
("auto-print-tip", ""),
("print-incoming-job-confirm-tip", ""),
("remote-printing-disallowed-tile-tip", ""),
("remote-printing-disallowed-text-tip", ""),
("save-settings-tip", ""),
("dont-show-again-tip", ""),
("Take screenshot", ""),
("Taking screenshot", ""),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", ""),
("Default trackpad speed", ""),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("Service", "Serviciu"),
("Start", "Pornește"),
("Stop", "Oprește"),
("exceed_max_devices", "Numărul maxim de dispozitive a fost depășit"),
("Sync with recent sessions", "Sincronizează cu sesiunile recente"),
("Sort tags", "Sortează etichete"),
("Open connection in new tab", "Deschide conexiunea într-o filă nouă"),
("Move tab to new window", "Mută fila într-o fereastră nouă"),
("Can not be empty", "Nu poate fi gol"),
("Already exists", "Există deja"),
("Change Password", "Schimbă parola"),
("Refresh Password", "Reîmprospătează parola"),
("ID", "ID"),
("Grid View", "Vizualizare grilă"),
("List View", "Vizualizare listă"),
("Select", "Selectează"),
("Toggle Tags", "Comută etichete"),
("pull_ab_failed_tip", "Sincronizarea agendei a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("push_ab_failed_tip", "Salvarea agendei pe server a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("synced_peer_readded_tip", "Dispozitivele pereche eliminate au fost re-adăugate automat din sesiunile recente."),
("Change Color", "Schimbă culoarea"),
("Primary Color", "Culoare principală"),
("HSV Color", "Culoare HSV"),
("Installation Successful!", "Instalare reușită!"),
("Installation failed!", "Instalare eșuată!"),
("Reverse mouse wheel", "Inversează rotiță mouse"),
("{} sessions", "{} sesiuni"),
("scam_title", "Avertisment de securitate"),
("scam_text1", "Escrocii se pot da drept angajați ai asistenței tehnice și îți pot solicita să instalezi sau să rulezi RustDesk pentru a-ți accesa dispozitivul."),
("scam_text2", "Dacă nu ai contactat tu primul asistența tehnică, te rugăm să închizi această aplicație imediat."),
("Don't show again", "Nu mai afișa"),
("I Agree", "Sunt de acord"),
("Decline", "Refuză"),
("Timeout in minutes", "Timp de expirare în minute"),
("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."),
("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"),
("Check for software update on startup", "Verifică actualizări la pornire"),
("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."),
("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("Filter by intersection", "Filtrează prin intersecție"),
("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"),
("Test", "Test"),
("display_is_plugged_out_msg", "Monitorul selectat a fost deconectat. Sesiunea continuă pe monitorul disponibil."),
("No displays", "Niciun monitor"),
("Open in new window", "Deschide în fereastră nouă"),
("Show displays as individual windows", "Afișează monitoarele ca ferestre individuale"),
("Use all my displays for the remote session", "Folosește toate monitoarele mele pentru sesiunea la distanță"),
("selinux_tip", "SELinux este activat pe acest sistem. Este posibil ca unele funcții să nu funcționeze corect. Te rugăm să consulți documentația pentru instrucțiuni de configurare."),
("Change view", "Schimbă vizualizarea"),
("Big tiles", "Dale mari"),
("Small tiles", "Dale mici"),
("List", "Listă"),
("Virtual display", "Monitor virtual"),
("Plug out all", "Deconectează toate"),
("True color (4:4:4)", "Culori reale (4:4:4)"),
("Enable blocking user input", "Activează blocarea intrărilor utilizatorului"),
("id_input_tip", "Introdu ID-ul sau adresa IP a dispozitivului la distanță"),
("privacy_mode_impl_mag_tip", "Modul privat prin Magnificare — nu este suportat pe toate sistemele"),
("privacy_mode_impl_virtual_display_tip", "Modul privat prin monitor virtual — necesită driverul de monitor virtual"),
("Enter privacy mode", "Intră în modul privat"),
("Exit privacy mode", "Ieși din modul privat"),
("idd_not_support_under_win10_2004_tip", "Driverul de monitor virtual nu este suportat pe versiuni de Windows anterioare versiunii 2004 (build 19041)."),
("input_source_1_tip", "Sursă de intrare 1 — folosește metodele standard de simulare a tastaturii și mouse-ului"),
("input_source_2_tip", "Sursă de intrare 2 — folosește driver-ul RustDesk pentru simulare la nivel de kernel"),
("Swap control-command key", "Schimbă tastele Control și Command"),
("swap-left-right-mouse", "Schimbă butoanele stâng și drept ale mouse-ului"),
("2FA code", "Cod 2FA"),
("More", "Mai mult"),
("enable-2fa-title", "Activează autentificarea în doi pași (2FA)"),
("enable-2fa-desc", "Scanează codul QR cu o aplicație de autentificare (de ex. Google Authenticator) și introdu codul generat pentru a confirma activarea."),
("wrong-2fa-code", "Cod 2FA incorect"),
("enter-2fa-title", "Introdu codul de autentificare în doi pași"),
("Email verification code must be 6 characters.", "Codul de verificare prin e-mail trebuie să aibă 6 caractere."),
("2FA code must be 6 digits.", "Codul 2FA trebuie să conțină 6 cifre."),
("Multiple Windows sessions found", "Au fost găsite mai multe sesiuni Windows"),
("Please select the session you want to connect to", "Selectează sesiunea la care vrei să te conectezi"),
("powered_by_me", "Realizat cu RustDesk"),
("outgoing_only_desk_tip", "Acest dispozitiv este configurat doar pentru conexiuni de ieșire și nu acceptă conexiuni de intrare."),
("preset_password_warning", "Parola prestabilită nu este recomandată din motive de securitate. Te rugăm să o schimbi cât mai curând posibil."),
("Security Alert", "Alertă de securitate"),
("My address book", "Agenda mea"),
("Personal", "Personal"),
("Owner", "Proprietar"),
("Set shared password", "Setează parola partajată"),
("Exist in", "Există în"),
("Read-only", "Doar citire"),
("Read/Write", "Citire/Scriere"),
("Full Control", "Control total"),
("share_warning_tip", "Datele partajate vor fi vizibile pentru toți membrii grupului selectat. Asigură-te că partajezi doar informații adecvate."),
("Everyone", "Toată lumea"),
("ab_web_console_tip", "Gestionează agenda prin consola web RustDesk Pro."),
("allow-only-conn-window-open-tip", "Permite conexiunile numai atunci când fereastra de gestionare a conexiunilor este deschisă"),
("no_need_privacy_mode_no_physical_displays_tip", "Modul privat nu este necesar deoarece nu există monitoare fizice conectate."),
("Follow remote cursor", "Urmărește cursorul de la distanță"),
("Follow remote window focus", "Urmărește fereastra activă de la distanță"),
("default_proxy_tip", "Proxy-ul implicit este utilizat pentru toate conexiunile dacă nu este specificat altul."),
("no_audio_input_device_tip", "Nu a fost găsit niciun dispozitiv de intrare audio. Conectează un microfon și reîncearcă."),
("Incoming", "Intrare"),
("Outgoing", "Ieșire"),
("Clear Wayland screen selection", "Șterge selecția de ecran Wayland"),
("clear_Wayland_screen_selection_tip", "Șterge selecția de ecran Wayland salvată, astfel încât să poți alege un alt ecran la următoarea conexiune."),
("confirm_clear_Wayland_screen_selection_tip", "Sigur vrei să ștergi selecția de ecran Wayland?"),
("android_new_voice_call_tip", "Ai primit un nou apel vocal. Apasă pentru a accepta sau respinge."),
("texture_render_tip", "Randarea prin textură poate îmbunătăți performanța grafică pe unele dispozitive. Repornește aplicația dacă apar probleme de afișare."),
("Use texture rendering", "Folosește randarea prin textură"),
("Floating window", "Fereastră flotantă"),
("floating_window_tip", "Fereastra flotantă ajută la menținerea serviciului de partajare a ecranului activ în fundal pe Android."),
("Keep screen on", "Menține ecranul pornit"),
("Never", "Niciodată"),
("During controlled", "În timpul controlului"),
("During service is on", "Cât timp serviciul este activ"),
("Capture screen using DirectX", "Capturează ecranul folosind DirectX"),
("Back", "Înapoi"),
("Apps", "Aplicații"),
("Volume up", "Mărește volumul"),
("Volume down", "Micșorează volumul"),
("Power", "Alimentare"),
("Telegram bot", "Bot Telegram"),
("enable-bot-tip", "Activează botul Telegram pentru a primi notificări și a gestiona conexiunile."),
("enable-bot-desc", "Configurează un bot Telegram pentru notificări RustDesk. Introdu token-ul botului și ID-ul chat-ului."),
("cancel-2fa-confirm-tip", "Sigur vrei să dezactivezi autentificarea în doi pași? Aceasta va reduce securitatea contului tău."),
("cancel-bot-confirm-tip", "Sigur vrei să dezactivezi botul Telegram?"),
("About RustDesk", "Despre RustDesk"),
("Send clipboard keystrokes", "Trimite conținutul clipboard-ului ca apăsări de taste"),
("network_error_tip", "Eroare de rețea. Verifică conexiunea la internet și încearcă din nou."),
("Unlock with PIN", "Deblochează cu PIN"),
("Requires at least {} characters", "Necesită cel puțin {} caractere"),
("Wrong PIN", "PIN incorect"),
("Set PIN", "Setează PIN"),
("Enable trusted devices", "Activează dispozitive de încredere"),
("Manage trusted devices", "Gestionează dispozitivele de încredere"),
("Platform", "Platformă"),
("Days remaining", "Zile rămase"),
("enable-trusted-devices-tip", "Dispozitivele de încredere pot accesa contul fără verificare suplimentară."),
("Parent directory", "Director părinte"),
("Resume", "Reia"),
("Invalid file name", "Nume de fișier nevalid"),
("one-way-file-transfer-tip", "Transferul de fișiere în sens unic permite doar trimiterea sau primirea de fișiere, nu ambele direcții simultan."),
("Authentication Required", "Autentificare necesară"),
("Authenticate", "Autentifică-te"),
("web_id_input_tip", "Introdu ID-ul RustDesk al dispozitivului la care vrei să te conectezi"),
("Download", "Descarcă"),
("Upload folder", "Încarcă folder"),
("Upload files", "Încarcă fișiere"),
("Clipboard is synchronized", "Clipboard-ul este sincronizat"),
("Update client clipboard", "Actualizează clipboard-ul clientului"),
("Untagged", "Neetichetat"),
("new-version-of-{}-tip", "Este disponibilă o nouă versiune a {}. Fă clic pentru a actualiza."),
("Accessible devices", "Dispozitive accesibile"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Versiunea clientului RustDesk de la distanță este mai mică decât {}. Te rugăm să o actualizezi pentru o compatibilitate completă."),
("d3d_render_tip", "Randarea Direct3D poate îmbunătăți performanța pe sistemele Windows cu suport hardware adecvat."),
("Use D3D rendering", "Folosește randarea D3D"),
("Printer", "Imprimantă"),
("printer-os-requirement-tip", "Imprimarea la distanță necesită Windows 10 sau o versiune superioară."),
("printer-requires-installed-{}-client-tip", "Imprimarea la distanță necesită instalarea clientului {} pe dispozitivul local."),
("printer-{}-not-installed-tip", "Imprimanta {} nu este instalată. Instalează driverul imprimantei pentru a continua."),
("printer-{}-ready-tip", "Imprimanta {} este pregătită pentru utilizare."),
("Install {} Printer", "Instalează imprimanta {}"),
("Outgoing Print Jobs", "Lucrări de imprimare de ieșire"),
("Incoming Print Jobs", "Lucrări de imprimare de intrare"),
("Incoming Print Job", "Lucrare de imprimare de intrare"),
("use-the-default-printer-tip", "Folosește imprimanta implicită a sistemului pentru lucrările de imprimare primite."),
("use-the-selected-printer-tip", "Folosește imprimanta selectată pentru lucrările de imprimare primite."),
("auto-print-tip", "Imprimă automat lucrările primite fără confirmare."),
("print-incoming-job-confirm-tip", "Ai primit o lucrare de imprimare. Vrei să o imprimești?"),
("remote-printing-disallowed-tile-tip", "Imprimare la distanță nepermisă"),
("remote-printing-disallowed-text-tip", "Dispozitivul la distanță nu permite imprimarea. Contactează administratorul pentru a activa această funcție."),
("save-settings-tip", "Salvează setările curente ca implicite pentru sesiunile viitoare."),
("dont-show-again-tip", "Nu mai afișa acest mesaj"),
("Take screenshot", "Fă captură de ecran"),
("Taking screenshot", "Se face captura de ecran..."),
("screenshot-merged-screen-not-supported-tip", "Captura de ecran a ecranului combinat nu este suportată în prezent."),
("screenshot-action-tip", "Selectează acțiunea pentru captura de ecran: salvează ca fișier sau copiază în clipboard."),
("Save as", "Salvează ca"),
("Copy to clipboard", "Copiază în clipboard"),
("Enable remote printer", "Activează imprimanta la distanță"),
("Downloading {}", "Se descarcă {}"),
("{} Update", "Actualizare {}"),
("{}-to-update-tip", "Este disponibilă o actualizare pentru {}. Fă clic pentru a descărca și instala."),
("download-new-version-failed-tip", "Descărcarea noii versiuni a eșuat. Verifică conexiunea la internet și încearcă din nou."),
("Auto update", "Actualizare automată"),
("update-failed-check-msi-tip", "Actualizarea a eșuat. Încearcă să descarci și să instalezi manual fișierul MSI."),
("websocket_tip", "WebSocket oferă o conexiune mai stabilă în unele medii de rețea restrictive."),
("Use WebSocket", "Folosește WebSocket"),
("Trackpad speed", "Viteza touchpad-ului"),
("Default trackpad speed", "Viteza implicită a touchpad-ului"),
("Numeric one-time password", "Parolă unică numerică"),
("Enable IPv6 P2P connection", "Activează conexiunea P2P prin IPv6"),
("Enable UDP hole punching", "Activează traversarea UDP (hole punching)"),
("View camera", "Vezi camera"),
("Enable camera", ""),
("No cameras", ""),
("view_camera_unsupported_tip", ""),
("Terminal", ""),
("Enable terminal", ""),
("New tab", ""),
("Keep terminal sessions on disconnect", ""),
("Terminal (Run as administrator)", ""),
("terminal-admin-login-tip", ""),
("Failed to get user token.", ""),
("Incorrect username or password.", ""),
("The user is not an administrator.", ""),
("Failed to check if the user is an administrator.", ""),
("Supported only in the installed version.", ""),
("elevation_username_tip", ""),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Enable camera", "Activează camera"),
("No cameras", "Nicio cameră disponibilă"),
("view_camera_unsupported_tip", "Vizualizarea camerei nu este suportată pe dispozitivul la distanță."),
("Terminal", "Terminal"),
("Enable terminal", "Activează terminalul"),
("New tab", "Filă nouă"),
("Keep terminal sessions on disconnect", "Păstrează sesiunile de terminal la deconectare"),
("Terminal (Run as administrator)", "Terminal (Rulează ca administrator)"),
("terminal-admin-login-tip", "Introdu datele de autentificare ale administratorului pentru a rula terminalul cu privilegii sporite."),
("Failed to get user token.", "Obținerea tokenului de utilizator a eșuat."),
("Incorrect username or password.", "Nume de utilizator sau parolă incorectă."),
("The user is not an administrator.", "Utilizatorul nu este administrator."),
("Failed to check if the user is an administrator.", "Verificarea privilegiilor de administrator a eșuat."),
("Supported only in the installed version.", "Suportat doar în versiunea instalată."),
("elevation_username_tip", "Introdu numele de utilizator al contului de administrator pentru a solicita sporirea privilegiilor."),
("Preparing for installation ...", "Se pregătește instalarea..."),
("Show my cursor", "Afișează cursorul meu"),
("Scale custom", "Scalare personalizată"),
("Custom scale slider", "Glisor pentru scalare personalizată"),
("Decrease", "Micșorează"),
("Increase", "Mărește"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Show virtual mouse", "Afișează mouse virtual"),
("Virtual mouse size", "Dimensiunea mouse-ului virtual"),
("Small", "Mic"),
("Large", "Mare"),
("Show virtual joystick", "Afișează joystick virtual"),
("Edit note", "Editează notă"),
("Alias", "Alias"),
("ScrollEdge", "Derulare la margine"),
("Allow insecure TLS fallback", "Permite revenirea la TLS nesecurizat"),
("allow-insecure-tls-fallback-tip", "Permite conexiunile cu certificate TLS nevalide sau expirate. Nu este recomandat din motive de securitate."),
("Disable UDP", "Dezactivează UDP"),
("disable-udp-tip", "Dezactivează conexiunile UDP și folosește doar TCP. Poate reduce performanța conexiunii."),
("server-oss-not-support-tip", "Serverul open-source nu suportă această funcție. Folosește RustDesk Pro pentru funcționalitate completă."),
("input note here", "Introdu o notă aici"),
("note-at-conn-end-tip", "Afișează această notă la sfârșitul sesiunii de conexiune."),
("Show terminal extra keys", "Afișează taste suplimentare pentru terminal"),
("Relative mouse mode", "Mod mouse relativ"),
("rel-mouse-not-supported-peer-tip", "Dispozitivul pereche nu suportă modul mouse relativ."),
("rel-mouse-not-ready-tip", "Modul mouse relativ nu este pregătit. Încearcă din nou."),
("rel-mouse-lock-failed-tip", "Blocarea mouse-ului în modul relativ a eșuat."),
("rel-mouse-exit-{}-tip", "Apasă {} pentru a ieși din modul mouse relativ."),
("rel-mouse-permission-lost-tip", "Permisiunea pentru modul mouse relativ a fost pierdută."),
("Changelog", "Jurnal de modificări"),
("keep-awake-during-outgoing-sessions-label", "Menține ecranul activ în timpul sesiunilor de ieșire"),
("keep-awake-during-incoming-sessions-label", "Menține ecranul activ în timpul sesiunilor de intrare"),
("Continue with {}", "Continuă cu {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Display Name", "Nume afișat"),
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
].iter().cloned().collect();
}

View File

@@ -666,7 +666,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incoming Print Job", "Входящее задание печати"),
("use-the-default-printer-tip", "Использовать принтер по умолчанию"),
("use-the-selected-printer-tip", "Использовать выбранный принтер"),
("auto-print-tip", "Автоматически выполнять печать на выбранном принтере."),
("auto-print-tip", "Автоматически выполнять печать на выбранном принтере"),
("print-incoming-job-confirm-tip", "Получено задание на печать с удалённого устройства. Выполнить его локально?"),
("remote-printing-disallowed-tile-tip", "Удалённая печать запрещена"),
("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."),
@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"),
("Continue with {}", "Продолжить с {}"),
("Display Name", "Отображаемое имя"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
].iter().cloned().collect();
}

View File

@@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranıık tutun"),
("Continue with {}", "{} ile devam et"),
("Display Name", "Görünen Ad"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("password-hidden-tip", "Şifre gizli"),
("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"),
].iter().cloned().collect();
}

View File

@@ -740,8 +740,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"),
("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"),
("Continue with {}", "使用 {} 登入"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Display Name", "顯示名稱"),
("password-hidden-tip", "固定密碼已設定(已隱藏)"),
("preset-password-in-use-tip", "目前正在使用預設密碼"),
].iter().cloned().collect();
}

View File

@@ -6,7 +6,7 @@ use hbb_common::{
anyhow::anyhow,
bail,
config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config},
libc::{c_char, c_int, c_long, c_uint, c_void},
libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void},
log,
message_proto::{DisplayInfo, Resolution},
regex::{Captures, Regex},
@@ -97,10 +97,55 @@ thread_local! {
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
}
// X11 error event structure for the custom error handler.
// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers
#[repr(C)]
struct XErrorEvent {
type_: c_int,
display: *mut c_void, // Display*
resourceid: c_ulong, // XID
serial: c_ulong,
error_code: u8,
request_code: u8,
minor_code: u8,
}
type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int;
const X11_BAD_WINDOW: u8 = 3;
const XDO_SUCCESS: c_int = 0;
const XDO_ERROR: c_int = 1;
/// Atomic flag set by the custom X error handler when a BadWindow error occurs.
static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false);
static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false);
/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of
/// letting the default handler terminate the process.
/// See issue: https://github.com/rustdesk/rustdesk/issues/9003
unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int {
if !event.is_null() && (*event).error_code == X11_BAD_WINDOW {
X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst);
log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed");
return 0;
}
X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst);
if !event.is_null() {
log::warn!(
"X11 error: error_code={}, request_code={}, minor_code={}",
(*event).error_code,
(*event).request_code,
(*event).minor_code,
);
}
0
}
#[link(name = "X11")]
extern "C" {
fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
// fn XCloseDisplay(d: *mut c_void) -> c_int;
fn XSetErrorHandler(handler: Option<XErrorHandler>) -> Option<XErrorHandler>;
}
#[link(name = "Xfixes")]
@@ -231,25 +276,47 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 {
return;
}
if libxdo_sys::xdo_get_window_location(
// XSetErrorHandler is process-global, not scoped to this Display/thread.
// This path is currently called by the single window_focus service thread.
// While installed, this handler can still observe unrelated X11 errors from
// other threads; unexpected errors make this geometry query fail.
X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst);
X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst);
let prev_handler = XSetErrorHandler(Some(handle_x_error));
let loc_ret = libxdo_sys::xdo_get_window_location(
*xdo as *const _,
window,
&mut x as _,
&mut y as _,
std::ptr::null_mut(),
) != 0
{
return;
}
if libxdo_sys::xdo_get_window_size(
*xdo as *const _,
window,
&mut width,
&mut height,
) != 0
);
let size_ret = if loc_ret == XDO_SUCCESS {
libxdo_sys::xdo_get_window_size(
*xdo as *const _,
window,
&mut width,
&mut height,
)
} else {
XDO_ERROR
};
// Do not call XSync(DISPLAY) here: DISPLAY is a separate
// XOpenDisplay() connection, while libxdo owns the Display*
// used by these geometry queries. These libxdo calls are
// synchronous XGetWindowAttributes-based queries, so the target
// BadWindow is expected to be delivered before the calls return.
XSetErrorHandler(prev_handler);
if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst)
|| X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst)
|| loc_ret != XDO_SUCCESS
|| size_ret != XDO_SUCCESS
{
return;
}
let center_x = x + (width / 2) as c_int;
let center_y = y + (height / 2) as c_int;
res = displays.iter().position(|d| {
@@ -2150,7 +2217,10 @@ pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> {
|| err_name == "org.freedesktop.DBus.Error.UnknownObject"
|| err_name == "org.freedesktop.DBus.Error.ServiceUnknown"
{
log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name);
log::info!(
"GNOME shortcuts inhibitor permission was not set ({})",
err_name
);
Ok(())
} else {
bail!("Failed to clear permission: {}", e)

View File

@@ -1472,7 +1472,7 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
let cur_exe = current_exe.to_str().unwrap_or("").to_owned();
let shortcut_icon_location = get_shortcut_icon_location(&cur_exe);
let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe);
let mk_shortcut = write_cmds(
format!(
"
@@ -1510,7 +1510,7 @@ oLink.Save
.to_str()
.unwrap_or("")
.to_owned();
let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?;
let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?;
let mut reg_value_desktop_shortcuts = "0".to_owned();
let mut reg_value_start_menu_shortcuts = "0".to_owned();
let mut reg_value_printer = "0".to_owned();
@@ -1621,7 +1621,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
{install_remote_printer}
{sleep}
",
display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()),
display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()),
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
after_install = get_after_install(
@@ -2125,12 +2125,16 @@ unsafe fn set_default_dll_directories() -> bool {
true
}
fn get_custom_icon(exe: &str) -> Option<String> {
fn get_custom_icon(install_dir: &str, exe: &str) -> Option<String> {
const RELATIVE_ICON_PATH: &str = "data\\flutter_assets\\assets\\icon.ico";
if crate::is_custom_client() {
if let Some(p) = PathBuf::from(exe).parent() {
let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico");
let alter_icon_path = p.join(RELATIVE_ICON_PATH);
if alter_icon_path.exists() {
// Verify that the icon is not a symlink for security
// During installation, files under `install_dir` may not exist yet.
// So we validate the icon from the current executable directory first.
// But for shortcut/registry icon location, we should point to the final
// installed path so the icon works across different Windows users.
if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) {
if metadata.is_symlink() {
log::warn!(
@@ -2140,7 +2144,11 @@ fn get_custom_icon(exe: &str) -> Option<String> {
return None;
}
if metadata.is_file() {
return Some(alter_icon_path.to_string_lossy().to_string());
return if install_dir.is_empty() {
Some(alter_icon_path.to_string_lossy().to_string())
} else {
Some(format!("{}\\{}", install_dir, RELATIVE_ICON_PATH))
};
}
}
}
@@ -2150,12 +2158,12 @@ fn get_custom_icon(exe: &str) -> Option<String> {
}
#[inline]
fn get_shortcut_icon_location(exe: &str) -> String {
fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String {
if exe.is_empty() {
return "".to_owned();
}
get_custom_icon(exe)
get_custom_icon(install_dir, exe)
.map(|p| format!("oLink.IconLocation = \"{}\"", p))
.unwrap_or_default()
}
@@ -2166,7 +2174,7 @@ pub fn create_shortcut(id: &str) -> ResultType<()> {
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
let filename = id.replace(':', "_");
let shortcut_icon_location = get_shortcut_icon_location(&exe);
let shortcut_icon_location = get_shortcut_icon_location("", &exe);
let shortcut = write_cmds(
format!(
"
@@ -2953,9 +2961,9 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
pub fn install_service() -> bool {
log::info!("Installing service...");
let _installing = crate::platform::InstallingService::new();
let (_, _, _, exe) = get_install_info();
let (_, path, _, exe) = get_install_info();
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
let tray_shortcut = get_tray_shortcut(&exe, &tmp_path).unwrap_or_default();
let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default();
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
Config::set_option("stop-service".into(), "".into());
crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed);
@@ -3064,7 +3072,8 @@ pub fn update_me(debug: bool) -> ResultType<()> {
let version = crate::VERSION.replace("-", ".");
let size = get_directory_size_kb(&path);
let build_date = crate::BUILD_DATE;
let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string());
// Use the icon in the previous installation directory if possible.
let display_icon = get_custom_icon("", &exe).unwrap_or(exe.to_string());
let is_msi = is_msi_installed().ok();
@@ -3421,8 +3430,13 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
Ok(())
}
pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
let shortcut_icon_location = get_shortcut_icon_location(exe);
pub fn get_tray_shortcut(
install_dir: &str,
exe: &str,
icon_source_exe: &str,
tmp_path: &str,
) -> ResultType<String> {
let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe);
Ok(write_cmds(
format!(
"

View File

@@ -1993,11 +1993,6 @@ impl Connection {
constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..])
}
#[inline]
fn validate_one_password(&self, password: &str) -> bool {
self.validate_password_plain(password)
}
fn validate_password_plain(&self, password: &str) -> bool {
if password.is_empty() {
return false;
@@ -2025,15 +2020,68 @@ impl Connection {
self.validate_password_plain(storage)
}
// This is coarse brute-force protection for the current temporary password value.
// We only care whether the active temporary password itself was presented correctly,
// not whether later authorization steps succeed. A successful temporary-password
// match clears this state immediately, and the counter also resets whenever the
// temporary password changes or is rotated.
fn check_update_temporary_password(&self, temporary_password_success: bool) {
const MAX_CONSECUTIVE_FAILURES: i32 = 10;
#[derive(Default)]
struct State {
password: String,
failures: i32,
}
lazy_static::lazy_static! {
static ref TEMPORARY_PASSWORD_FAILURES: Mutex<State> =
Mutex::new(State::default());
}
if !password::temporary_enabled() {
return;
}
let mut state = TEMPORARY_PASSWORD_FAILURES.lock().unwrap();
let current_password = password::temporary_password();
if current_password.is_empty() {
return;
}
if state.password != current_password {
state.password = current_password;
state.failures = 0;
}
if temporary_password_success {
state.failures = 0;
return;
}
state.failures += 1;
if state.failures < MAX_CONSECUTIVE_FAILURES {
return;
}
password::update_temporary_password();
let new_password = password::temporary_password();
log::warn!(
"Temporary password rotated after too many consecutive wrong attempts: failures={}, ip={}",
state.failures,
self.ip,
);
state.password = new_password;
state.failures = 0;
}
fn validate_password(&mut self, allow_permanent_password: bool) -> bool {
if password::temporary_enabled() {
let password = password::temporary_password();
if self.validate_one_password(&password) {
if self.validate_password_plain(&password) {
raii::AuthedConnID::update_or_insert_session(
self.session_key(),
Some(password),
Some(false),
);
self.check_update_temporary_password(true);
return true;
}
}
@@ -2406,6 +2454,7 @@ impl Connection {
}
if !self.validate_password(allow_logon_screen_password) {
self.update_failure(failure, false, 0);
self.check_update_temporary_password(false);
if err_msg.is_empty() {
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
.await;

View File

@@ -52,12 +52,12 @@ impl InvokeUiCM for SciterHandler {
self.call("newMessage", &make_args!(id, text));
}
fn change_theme(&self, _dark: String) {
// TODO
fn change_theme(&self, dark: String) {
self.call("changeTheme", &make_args!(dark));
}
fn change_language(&self) {
// TODO
self.call("changeLanguage", &make_args!());
}
fn show_elevation(&self, show: bool) {

View File

@@ -602,7 +602,13 @@ function togglePrivacyMode(privacy_id) {
if (!supported) {
msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { });
} else {
handler.toggle_option(privacy_id);
var privacy_mode_impls = pi.platform_additions?.supported_privacy_mode_impl;
if (privacy_mode_impls == null || privacy_mode_impls == undefined) {
handler.toggle_option(privacy_id);
return;
}
var is_on = handler.get_toggle_option("privacy-mode");
handler.toggle_privacy_mode("", !is_on);
}
}
@@ -713,4 +719,4 @@ handler.setConnectionType = function(secured, direct, stream_type) {
handler.updateRecordStatus = function(status) {
recording = status;
header.update();
}
}

View File

@@ -85,6 +85,22 @@ impl SciterHandler {
serde_json::Value::Bool(b) => {
value.set_item(k, b);
}
serde_json::Value::Array(arr) if k == "supported_privacy_mode_impl" => {
let mut impls = Value::array(0);
for item in arr {
if let serde_json::Value::Array(entry) = item {
let impl_key = entry.get(0).and_then(|v| v.as_str());
let impl_name = entry.get(1).and_then(|v| v.as_str());
if let (Some(impl_key), Some(impl_name)) = (impl_key, impl_name) {
let mut impl_item = Value::array(0);
impl_item.push(impl_key);
impl_item.push(impl_name);
impls.push(impl_item);
}
}
}
value.set_item(k, impls);
}
_ => {
// ignore for now
}
@@ -550,6 +566,7 @@ impl sciter::EventHandler for SciterSession {
fn get_toggle_option(String);
fn is_privacy_mode_supported();
fn toggle_option(String);
fn toggle_privacy_mode(String, bool);
fn get_remember();
fn peer_platform();
fn set_write_override(i32, i32, bool, bool, bool);

View File

@@ -941,15 +941,6 @@ async fn handle_fs(
total_size,
conn_id,
} => {
// Validate file names to prevent path traversal attacks.
// This must be done BEFORE any path operations to ensure attackers cannot
// escape the target directory using names like "../../malicious.txt"
if let Err(e) = validate_transfer_file_names(&files) {
log::warn!("Path traversal attempt detected for {}: {}", path, e);
send_raw(fs::new_error(id, e, file_num), tx);
return;
}
// Convert files to FileEntry
let file_entries: Vec<FileEntry> = files
.drain(..)
@@ -970,9 +961,13 @@ async fn handle_fs(
file_num,
false,
false,
file_entries,
overwrite_detection,
);
if let Err(e) = job.set_files(file_entries) {
log::warn!("Reject unsafe transfer file list for {}: {}", path, e);
send_raw(fs::new_error(id, e, file_num), tx);
return;
}
job.total_size = total_size;
job.conn_id = conn_id;
write_jobs.push(job);
@@ -1160,73 +1155,6 @@ async fn handle_fs(
}
}
/// Validates that a file name does not contain path traversal sequences.
/// This prevents attackers from escaping the base directory by using names like
/// "../../../etc/passwd" or "..\\..\\Windows\\System32\\malicious.dll".
#[cfg(not(any(target_os = "ios")))]
fn validate_file_name_no_traversal(name: &str) -> ResultType<()> {
// Check for null bytes which could cause path truncation in some APIs
if name.bytes().any(|b| b == 0) {
bail!("file name contains null bytes");
}
// Check for path traversal patterns
// We check for both Unix and Windows path separators
if name
.split(|c| c == '/' || c == '\\')
.filter(|s| !s.is_empty())
.any(|component| component == "..")
{
bail!("path traversal detected in file name");
}
// On Windows, also check for drive letters (e.g., "C:")
#[cfg(windows)]
{
if name.len() >= 2 {
let bytes = name.as_bytes();
if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
bail!("absolute path detected in file name");
}
}
}
// Check for names starting with path separator:
// - Unix absolute paths (e.g., "/etc/passwd")
// - Windows UNC paths (e.g., "\\server\share")
if name.starts_with('/') || name.starts_with('\\') {
bail!("absolute path detected in file name");
}
Ok(())
}
#[inline]
fn is_single_file_with_empty_name(files: &[(String, u64)]) -> bool {
files.len() == 1 && files.first().map_or(false, |f| f.0.is_empty())
}
/// Validates all file names in a transfer request to prevent path traversal attacks.
/// Returns an error if any file name contains dangerous path components.
#[cfg(not(any(target_os = "ios")))]
fn validate_transfer_file_names(files: &[(String, u64)]) -> ResultType<()> {
if is_single_file_with_empty_name(files) {
// Allow empty name for single file.
// The full path is provided in the `path` parameter for single file transfers.
return Ok(());
}
for (name, _) in files {
// In multi-file transfers, empty names are not allowed.
// Each file must have a valid name to construct the destination path.
if name.is_empty() {
bail!("empty file name in multi-file transfer");
}
validate_file_name_no_traversal(name)?;
}
Ok(())
}
/// Start a read job in CM for file transfer from server to client (Windows only).
///
/// This creates a `TransferJob` using `new_read()`, validates it, and sends the
@@ -1601,16 +1529,7 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender<Data>) {
#[cfg(not(any(target_os = "ios")))]
async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender<Data>) {
handle_result(
spawn_blocking(move || {
// Rename target must not be empty
if new_name.is_empty() {
bail!("new file name cannot be empty");
}
// Validate that new_name doesn't contain path traversal
validate_file_name_no_traversal(&new_name)?;
fs::rename_file(&path, &new_name)
})
.await,
spawn_blocking(move || fs::rename_file(&path, &new_name)).await,
id,
0,
tx,
@@ -1773,42 +1692,6 @@ mod tests {
});
}
#[test]
#[cfg(not(any(target_os = "ios")))]
fn validate_file_name_security() {
// Null byte injection
assert!(super::validate_file_name_no_traversal("file\0.txt").is_err());
assert!(super::validate_file_name_no_traversal("test\0").is_err());
// Path traversal
assert!(super::validate_file_name_no_traversal("../etc/passwd").is_err());
assert!(super::validate_file_name_no_traversal("foo/../bar").is_err());
assert!(super::validate_file_name_no_traversal("..").is_err());
// Absolute paths
assert!(super::validate_file_name_no_traversal("/etc/passwd").is_err());
assert!(super::validate_file_name_no_traversal("\\Windows").is_err());
#[cfg(windows)]
assert!(super::validate_file_name_no_traversal("C:\\Windows").is_err());
// Valid paths
assert!(super::validate_file_name_no_traversal("file.txt").is_ok());
assert!(super::validate_file_name_no_traversal("subdir/file.txt").is_ok());
assert!(super::validate_file_name_no_traversal("").is_ok());
}
#[test]
#[cfg(not(any(target_os = "ios")))]
fn validate_transfer_file_names_security() {
assert!(super::validate_transfer_file_names(&[("file.txt".into(), 100)]).is_ok());
assert!(super::validate_transfer_file_names(&[("".into(), 100)]).is_ok());
assert!(
super::validate_transfer_file_names(&[("".into(), 100), ("file.txt".into(), 100)])
.is_err()
);
assert!(super::validate_transfer_file_names(&[("../passwd".into(), 100)]).is_err());
}
/// Tests that symlink creation works on this platform.
/// This is a helper to verify the test environment supports symlinks.
#[test]

View File

@@ -23,7 +23,7 @@ use hbb_common::{
sync::mpsc,
time::{Duration as TokioDuration, Instant},
},
whoami, Stream,
whoami, SessionID, Stream,
};
use rdev::{Event, EventType::*, KeyCode};
#[cfg(all(feature = "vram", feature = "flutter"))]
@@ -870,12 +870,14 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn enter(&self, keyboard_mode: String) {
keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode);
let session_id = self.lc.read().unwrap().session_id as u128;
keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode, session_id);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn leave(&self, keyboard_mode: String) {
keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode);
let session_id = self.lc.read().unwrap().session_id as u128;
keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode, session_id);
}
// flutter only TODO new input
@@ -911,6 +913,7 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(any(target_os = "ios"))]
pub fn handle_flutter_raw_key_event(
&self,
_session_id: SessionID,
_keyboard_mode: &str,
_name: &str,
_platform_code: i32,
@@ -923,6 +926,7 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(not(any(target_os = "ios")))]
pub fn handle_flutter_raw_key_event(
&self,
session_id: SessionID,
keyboard_mode: &str,
name: &str,
platform_code: i32,
@@ -934,6 +938,7 @@ impl<T: InvokeUiSession> Session<T> {
self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up);
} else {
self._handle_raw_key_non_flutter_simulation(
session_id,
keyboard_mode,
platform_code,
position_code,
@@ -946,6 +951,7 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(not(any(target_os = "ios")))]
fn _handle_raw_key_non_flutter_simulation(
&self,
session_id: SessionID,
keyboard_mode: &str,
platform_code: i32,
position_code: i32,
@@ -979,11 +985,18 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(any(target_os = "windows", target_os = "macos"))]
extra_data: 0,
};
keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self);
keyboard::client::process_event_with_session(
keyboard_mode,
&event,
Some(lock_modes),
self,
session_id,
);
}
pub fn handle_flutter_key_event(
&self,
session_id: SessionID,
keyboard_mode: &str,
character: &str,
usb_hid: i32,
@@ -994,6 +1007,7 @@ impl<T: InvokeUiSession> Session<T> {
self._handle_key_flutter_simulation(keyboard_mode, usb_hid, down_or_up);
} else {
self._handle_key_non_flutter_simulation(
session_id,
keyboard_mode,
character,
usb_hid,
@@ -1029,6 +1043,7 @@ impl<T: InvokeUiSession> Session<T> {
fn _handle_key_non_flutter_simulation(
&self,
session_id: SessionID,
keyboard_mode: &str,
character: &str,
usb_hid: i32,
@@ -1090,7 +1105,13 @@ impl<T: InvokeUiSession> Session<T> {
#[cfg(any(target_os = "windows", target_os = "macos"))]
extra_data: 0,
};
keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self);
keyboard::client::process_event_with_session(
keyboard_mode,
&event,
Some(lock_modes),
self,
session_id,
);
}
// flutter only TODO new input