mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-07 14:48:11 +03:00
Compare commits
58 Commits
43df9fb7a1
...
keyboard-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb097012b3 | ||
|
|
55ff1cd8c8 | ||
|
|
d403d640f8 | ||
|
|
42a88ac1f0 | ||
|
|
cd7686baa2 | ||
|
|
68e07ed7eb | ||
|
|
d4a1430c27 | ||
|
|
bfd31d21e4 | ||
|
|
590296b297 | ||
|
|
ee8cc0c06b | ||
|
|
99b565ef40 | ||
|
|
1e6a3dc644 | ||
|
|
5b7ad339b8 | ||
|
|
7308c448f1 | ||
|
|
c8ba99d1a1 | ||
|
|
5ea6714db8 | ||
|
|
3a1622e8b5 | ||
|
|
38f1300717 | ||
|
|
03e351ac61 | ||
|
|
6cb323725b | ||
|
|
5d0533f0d4 | ||
|
|
e0c5e1483e | ||
|
|
47e4c65d8e | ||
|
|
9bc1ce52af | ||
|
|
348d1b46e1 | ||
|
|
1a41b3ac11 | ||
|
|
b239535009 | ||
|
|
5fd20f808c | ||
|
|
803ac8cc4e | ||
|
|
4a50bc6fc2 | ||
|
|
e8a1b7fe21 | ||
|
|
ac124c0680 | ||
|
|
91aff3ffd1 | ||
|
|
642c281ad0 | ||
|
|
1e9c4d04f1 | ||
|
|
9f817714fe | ||
|
|
091f2c6135 | ||
|
|
91de51290d | ||
|
|
68fa0466c8 | ||
|
|
28e303576c | ||
|
|
2d41b3e80d | ||
|
|
ffd2d26c1a | ||
|
|
a8dc6fc632 | ||
|
|
771cb4ebd7 | ||
|
|
2f694c0eb2 | ||
|
|
8dea347a21 | ||
|
|
0cf3e8ed40 | ||
|
|
9d3bc7d9e6 | ||
|
|
e0427bdc77 | ||
|
|
9cf1338dc4 | ||
|
|
4e30ee8d1c | ||
|
|
cca6a5fe12 | ||
|
|
9e4b7fca4d | ||
|
|
d135c58ead | ||
|
|
de194417d4 | ||
|
|
d01ce3173f | ||
|
|
010a54d1c9 | ||
|
|
f557fc94fa |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
86
AGENTS.md
Normal 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.
|
||||
92
CLAUDE.md
92
CLAUDE.md
@@ -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
|
||||
|
||||
@@ -245,3 +245,6 @@ panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
||||
|
||||
[profile.dev]
|
||||
debug = 1
|
||||
|
||||
2
build.py
2
build.py
@@ -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
143
docs/CODE_OF_CONDUCT-FR.md
Normal 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
55
docs/CONTRIBUTING-FR.md
Normal 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).
|
||||
@@ -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
16
docs/SECURITY-FR.md
Normal 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é.
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
1
flutter/assets/auth-microsoft.svg
Normal file
1
flutter/assets/auth-microsoft.svg
Normal 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 |
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
111
flutter/lib/common/widgets/keyboard_shortcuts/display.dart
Normal file
111
flutter/lib/common/widgets/keyboard_shortcuts/display.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// flutter/lib/common/widgets/keyboard_shortcuts/display.dart
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../../consts.dart';
|
||||
import '../../../models/platform_model.dart';
|
||||
import 'shortcut_utils.dart';
|
||||
|
||||
/// Read the bindings JSON and produce a human-readable shortcut string for
|
||||
/// `actionId`, formatted for the current OS. Returns null if unbound, or —
|
||||
/// when [requireEnabled] is true (the default) — when the master toggle is
|
||||
/// off. The configuration page passes `requireEnabled: false` so users still
|
||||
/// see what they have bound while the feature is disabled.
|
||||
class ShortcutDisplay {
|
||||
// Cache parsed JSON keyed by the raw string — called per visible action on
|
||||
// every menu rebuild, so the jsonDecode is the real cost. Invalidation is
|
||||
// automatic: a write changes the raw and we re-parse.
|
||||
static String? _cachedRaw;
|
||||
static Map<String, dynamic>? _cachedParsed;
|
||||
|
||||
@visibleForTesting
|
||||
static void resetCache() {
|
||||
_cachedRaw = null;
|
||||
_cachedParsed = null;
|
||||
}
|
||||
|
||||
static String? formatFor(String actionId, {bool requireEnabled = true}) {
|
||||
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
||||
if (raw.isEmpty) return null;
|
||||
Map<String, dynamic>? parsed;
|
||||
if (raw == _cachedRaw) {
|
||||
parsed = _cachedParsed;
|
||||
} else {
|
||||
try {
|
||||
parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
parsed = null;
|
||||
}
|
||||
_cachedRaw = raw;
|
||||
_cachedParsed = parsed;
|
||||
}
|
||||
if (parsed == null) return null;
|
||||
if (requireEnabled && parsed['enabled'] != true) return null;
|
||||
// When pass-through is on, the matcher returns early on every keystroke.
|
||||
// Showing the bound combo next to a menu item would lie to the user — they
|
||||
// would press it expecting the local action and instead the keys would go
|
||||
// to the remote. Treat as unbound for display purposes.
|
||||
if (requireEnabled && parsed['pass_through'] == true) return null;
|
||||
final list = shortcutBindingMapsFrom(parsed['bindings']);
|
||||
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>[];
|
||||
// Plain-text labels (Cmd / Ctrl / Alt / Shift) instead of Unicode glyphs
|
||||
// (⌘ ⌃ ⌥ ⇧). Flutter Web's CanvasKit bundled fonts don't always carry the
|
||||
// macOS modifier symbols, which renders as garbled boxes on Mac browsers;
|
||||
// text is portable and readable on every platform.
|
||||
//
|
||||
// Order matches the canonical macOS order (Cmd, Control, Option, Shift)
|
||||
// so the rendered hint reads naturally. `ctrl` only ever appears in
|
||||
// saved bindings on macOS — Win/Linux collapses Ctrl into `primary`.
|
||||
final parts = <String>[];
|
||||
for (final m in ['primary', 'ctrl', 'alt', 'shift']) {
|
||||
if (!mods.contains(m)) continue;
|
||||
switch (m) {
|
||||
case 'primary': parts.add(isMac ? 'Cmd' : 'Ctrl'); break;
|
||||
case 'ctrl': parts.add(isMac ? 'Control' : 'Ctrl'); break;
|
||||
case 'alt': parts.add(isMac ? 'Option' : 'Alt'); break;
|
||||
case 'shift': parts.add('Shift'); break;
|
||||
}
|
||||
}
|
||||
parts.add(_keyDisplay(keyValue));
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
static String _keyDisplay(String key) {
|
||||
switch (key) {
|
||||
case 'delete': return 'Del';
|
||||
case 'backspace': return 'Backspace';
|
||||
case 'enter': return 'Enter';
|
||||
case 'tab': return 'Tab';
|
||||
case 'space': return 'Space';
|
||||
case 'arrow_left': return 'Left';
|
||||
case 'arrow_right':return 'Right';
|
||||
case 'arrow_up': return 'Up';
|
||||
case 'arrow_down': return 'Down';
|
||||
case 'home': return 'Home';
|
||||
case 'end': return 'End';
|
||||
case 'page_up': return 'PgUp';
|
||||
case 'page_down': return 'PgDn';
|
||||
case 'insert': return 'Ins';
|
||||
}
|
||||
if (key.startsWith('digit')) return key.substring(5);
|
||||
// F-keys ("f1".."f12") and single letters fall through to uppercase.
|
||||
return key.toUpperCase();
|
||||
}
|
||||
}
|
||||
481
flutter/lib/common/widgets/keyboard_shortcuts/page_body.dart
Normal file
481
flutter/lib/common/widgets/keyboard_shortcuts/page_body.dart
Normal file
@@ -0,0 +1,481 @@
|
||||
// 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 'display.dart';
|
||||
import 'recording_dialog.dart';
|
||||
import 'shortcut_actions.dart';
|
||||
import 'shortcut_utils.dart';
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Whether to render the master Enable + Pass-through toggles inside the
|
||||
/// body. Desktop shells set this to false because the General settings tab
|
||||
/// already exposes both checkboxes (and is the only entry point to this
|
||||
/// page on desktop). Mobile defaults to true: its entry point is a plain
|
||||
/// nav tile in Settings, so this page is the only place the user can
|
||||
/// flip the master switches.
|
||||
final bool showMasterToggles;
|
||||
|
||||
const KeyboardShortcutsPageBody({
|
||||
Key? key,
|
||||
this.compact = true,
|
||||
this.editButtonHint,
|
||||
this.headerBanner,
|
||||
this.showMasterToggles = true,
|
||||
}) : 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 = shortcutBindingMapsFrom(json['bindings']);
|
||||
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 {
|
||||
await ShortcutModel.setEnabled(v);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _setPassThrough(bool v) async {
|
||||
await ShortcutModel.setPassThrough(v);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _resetToDefaults() async {
|
||||
final json = _readJson();
|
||||
// Single source of truth lives in `ShortcutModel.currentPlatformCapabilities`
|
||||
// — the same helper feeds the first-enable seed pass, this Reset action,
|
||||
// and the action-list filter below, so the three can never disagree on
|
||||
// which actions belong on this platform.
|
||||
json['bindings'] = filterDefaultBindingsForPlatform(
|
||||
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
|
||||
ShortcutModel.currentPlatformCapabilities(),
|
||||
);
|
||||
await _writeJson(json);
|
||||
}
|
||||
|
||||
String _labelFor(String actionId) {
|
||||
// Intentionally walks the unfiltered list (via the recursive helper, so
|
||||
// both direct entries and subgroup entries are covered) — a stale
|
||||
// cross-platform binding (e.g. Toggle Toolbar carried over from
|
||||
// desktop) should still resolve to its human-readable label in conflict
|
||||
// warnings.
|
||||
for (final entry in allActionEntries(kKeyboardShortcutActionGroups)) {
|
||||
if (entry.id == actionId) return translate(entry.labelKey);
|
||||
}
|
||||
return actionId;
|
||||
}
|
||||
|
||||
/// Action groups visible on the current platform. Reads the same
|
||||
/// capability set as the seed-defaults / reset-to-defaults paths from
|
||||
/// `ShortcutModel.currentPlatformCapabilities`, so the UI lists exactly
|
||||
/// the actions whose handlers the matcher can dispatch here.
|
||||
List<KeyboardShortcutActionGroup> _groupsForCurrentPlatform() {
|
||||
return filterKeyboardShortcutActionGroupsForPlatform(
|
||||
ShortcutModel.currentPlatformCapabilities(),
|
||||
);
|
||||
}
|
||||
|
||||
// ----- UI handlers -----
|
||||
|
||||
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
|
||||
final json = _readJson();
|
||||
final bindings = shortcutBindingMapsFrom(json['bindings']);
|
||||
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),
|
||||
],
|
||||
if (widget.showMasterToggles) ...[
|
||||
_toggleRow(
|
||||
enabled,
|
||||
'Enable keyboard shortcuts in remote session',
|
||||
(v) => _setEnabled(v),
|
||||
),
|
||||
if (enabled)
|
||||
_toggleRow(
|
||||
ShortcutModel.isPassThrough(),
|
||||
'Pass-through to remote',
|
||||
(v) => _setPassThrough(v),
|
||||
),
|
||||
],
|
||||
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),
|
||||
// Bindings list and configuration entry only show when shortcuts are
|
||||
// enabled — there is nothing to configure while the matcher is off.
|
||||
if (enabled)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final group in _groupsForCurrentPlatform())
|
||||
_buildGroup(context, group),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toggleRow(
|
||||
bool value, String labelKey, Future<void> Function(bool) onChanged,
|
||||
{String? tooltipKey}) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: value,
|
||||
onChanged: (v) async {
|
||||
if (v == null) return;
|
||||
await onChanged(v);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => onChanged(!value),
|
||||
child: Text(translate(labelKey)),
|
||||
),
|
||||
),
|
||||
if (tooltipKey != null) InfoTooltipIcon(tipKey: tooltipKey),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// One indent unit per nesting level. Both "top item under top heading"
|
||||
// and "subgroup heading under top group" are *one* level deeper than the
|
||||
// top heading, so they share this indent — meaning a top-level direct
|
||||
// item and a sibling subgroup heading line up at exactly the same x.
|
||||
// Subgroup items are *two* levels deeper.
|
||||
static const double _kIndentStep = 16.0;
|
||||
|
||||
/// Top-level group: heading at zero indent, then walk `children` in
|
||||
/// declaration order. Direct entries get [_kIndentStep] of indent so
|
||||
/// they read as "items under this heading"; subgroup headings sit at
|
||||
/// the same indent (a subgroup is a sibling of the direct items, just
|
||||
/// with its own nested entries below).
|
||||
Widget _buildGroup(BuildContext context, KeyboardShortcutActionGroup group) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
_buildHeading(context, group.titleKey, isSub: false),
|
||||
const SizedBox(height: 4),
|
||||
for (final child in group.children)
|
||||
switch (child) {
|
||||
KeyboardShortcutActionEntry() => Padding(
|
||||
padding: const EdgeInsets.only(left: _kIndentStep),
|
||||
child: _buildEntryRow(context, child),
|
||||
),
|
||||
KeyboardShortcutActionSubgroup() =>
|
||||
_buildSubgroup(context, child),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubgroup(
|
||||
BuildContext context, KeyboardShortcutActionSubgroup subgroup) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
_buildHeading(context, subgroup.titleKey, isSub: true),
|
||||
const SizedBox(height: 4),
|
||||
for (final entry in subgroup.entries)
|
||||
Padding(
|
||||
// Two indent steps: one for "subgroup heading is nested under
|
||||
// top heading" (matches the heading's own indent) and one for
|
||||
// "this entry is under the subgroup heading".
|
||||
padding: const EdgeInsets.only(left: _kIndentStep * 2),
|
||||
child: _buildEntryRow(context, entry),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeading(BuildContext context, String titleKey,
|
||||
{required bool isSub}) {
|
||||
// Subgroup heading nests one step under the top heading — same indent
|
||||
// as a top-level direct item, so the two line up at the same x.
|
||||
final indent = isSub ? _kIndentStep : 0.0;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8 + indent, right: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate(titleKey),
|
||||
style: TextStyle(
|
||||
fontWeight: isSub ? FontWeight.w500 : FontWeight.w600,
|
||||
color: isSub
|
||||
? Theme.of(context).hintColor
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Divider(thickness: isSub ? 0.5 : 1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntryRow(
|
||||
BuildContext context, KeyboardShortcutActionEntry entry) {
|
||||
return widget.compact
|
||||
? _buildCompactRow(context, entry)
|
||||
: _buildTouchRow(context, entry);
|
||||
}
|
||||
|
||||
/// Desktop dense row: label | shortcut | edit | clear, all in one Row.
|
||||
Widget _buildCompactRow(
|
||||
BuildContext context, KeyboardShortcutActionEntry entry) {
|
||||
final shortcut = ShortcutDisplay.formatFor(entry.id, requireEnabled: false);
|
||||
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 = ShortcutDisplay.formatFor(entry.id, requireEnabled: false);
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Small help-icon tooltip used for inline explanations next to a checkbox /
|
||||
/// row. Triggers on hover (desktop) and tap (mobile). Public so the desktop
|
||||
/// General settings tab can reuse it.
|
||||
class InfoTooltipIcon extends StatelessWidget {
|
||||
final String tipKey;
|
||||
const InfoTooltipIcon({Key? key, required this.tipKey}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: translate(tipKey),
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
preferBelow: false,
|
||||
waitDuration: const Duration(milliseconds: 250),
|
||||
showDuration: const Duration(seconds: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
// 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 that at least one
|
||||
// modifier is present, 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';
|
||||
import 'shortcut_utils.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;
|
||||
|
||||
// Human-readable label for the most recent press that we couldn't bind to
|
||||
// (e.g. F13, media keys). null when the last press was either supported or
|
||||
// a modifier-only press. Cleared whenever a supported key arrives, so a
|
||||
// user who hits an unsupported key after a valid capture sees the warning
|
||||
// until they press something else. Distinct from `_key == null` so the
|
||||
// status line can tell the user *why* their press was ignored instead of
|
||||
// silently doing nothing.
|
||||
String? _unsupportedKey;
|
||||
|
||||
// Modifier LogicalKeyboardKeys we should *not* treat as "unsupported" when
|
||||
// they fail to map to a key name. A modifier-only press is normal during
|
||||
// combo capture (the user is building up their combo) — only non-modifier
|
||||
// unmapped keys deserve the warning.
|
||||
static final _modifierKeys = <LogicalKeyboardKey>{
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.shiftLeft,
|
||||
LogicalKeyboardKey.shiftRight,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.controlRight,
|
||||
LogicalKeyboardKey.alt,
|
||||
LogicalKeyboardKey.altLeft,
|
||||
LogicalKeyboardKey.altRight,
|
||||
LogicalKeyboardKey.meta,
|
||||
LogicalKeyboardKey.metaLeft,
|
||||
LogicalKeyboardKey.metaRight,
|
||||
LogicalKeyboardKey.capsLock,
|
||||
LogicalKeyboardKey.numLock,
|
||||
LogicalKeyboardKey.scrollLock,
|
||||
LogicalKeyboardKey.fn,
|
||||
LogicalKeyboardKey.fnLock,
|
||||
};
|
||||
|
||||
@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 at least one modifier. Lower bound
|
||||
/// for any sensible binding — pure single-key bindings would swallow normal
|
||||
/// typing the moment shortcuts are enabled. Beyond one mod the user is on
|
||||
/// their own; the in-session pass-through toggle is the escape hatch when
|
||||
/// a chosen combo collides with something needed on the remote.
|
||||
bool get _hasRequiredPrefix => _mods.isNotEmpty;
|
||||
|
||||
/// 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 = shortcutModSetFrom(b['mods']);
|
||||
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 = logicalKeyName(logical);
|
||||
|
||||
// Mirror of `normalize_modifiers` in src/keyboard/shortcuts.rs:
|
||||
// * macOS: Cmd → primary, Ctrl → ctrl (distinct).
|
||||
// * Win/Linux: Ctrl → primary, no separate Ctrl modifier.
|
||||
// The two halves must agree on labels, otherwise saved bindings will not
|
||||
// match the events the matcher sees at runtime.
|
||||
final mods = <String>{};
|
||||
if (HardwareKeyboard.instance.isAltPressed) mods.add('alt');
|
||||
if (HardwareKeyboard.instance.isShiftPressed) mods.add('shift');
|
||||
if (_isMac) {
|
||||
if (HardwareKeyboard.instance.isMetaPressed) mods.add('primary');
|
||||
if (HardwareKeyboard.instance.isControlPressed) mods.add('ctrl');
|
||||
} else {
|
||||
if (HardwareKeyboard.instance.isControlPressed) 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;
|
||||
_unsupportedKey = null;
|
||||
} else if (!_modifierKeys.contains(logical)) {
|
||||
// Non-modifier key we don't recognize (e.g. F13, media keys, IME
|
||||
// compose keys). Surface a warning instead of silently dropping the
|
||||
// press — the dialog otherwise looks unresponsive.
|
||||
final label = logical.keyLabel.isNotEmpty
|
||||
? logical.keyLabel
|
||||
: (logical.debugName ?? 'this key');
|
||||
_unsupportedKey = label;
|
||||
}
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void _onSave() {
|
||||
if (_key == null || !_hasRequiredPrefix) return;
|
||||
final ordered = canonicalShortcutModsForSave(_mods);
|
||||
final binding = <String, dynamic>{
|
||||
'action': widget.actionId,
|
||||
'mods': ordered,
|
||||
'key': _key!,
|
||||
};
|
||||
Navigator.of(context).pop(RecordingResult(binding, _conflictActionId));
|
||||
}
|
||||
|
||||
String _formatPrefix() {
|
||||
// Used in the "must include..." validation row; lists the modifier set
|
||||
// a binding can pick from. Localised modifier glyphs aren't used here so
|
||||
// the names stay greppable for users searching for "Option" / "Cmd".
|
||||
if (_isMac) return 'Cmd / Control / Option / Shift';
|
||||
return 'Ctrl / Alt / Shift';
|
||||
}
|
||||
|
||||
String _formatCombo() {
|
||||
// Plain-text labels (see same rationale in display.dart::_keyDisplay).
|
||||
final parts = <String>[];
|
||||
for (final m in ['primary', 'ctrl', 'alt', 'shift']) {
|
||||
if (!_mods.contains(m)) continue;
|
||||
switch (m) {
|
||||
case 'primary':
|
||||
parts.add(_isMac ? 'Cmd' : 'Ctrl');
|
||||
break;
|
||||
case 'ctrl':
|
||||
parts.add(_isMac ? 'Control' : 'Ctrl');
|
||||
break;
|
||||
case 'alt':
|
||||
parts.add(_isMac ? 'Option' : 'Alt');
|
||||
break;
|
||||
case 'shift':
|
||||
parts.add('Shift');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_key != null) {
|
||||
parts.add(_keyDisplay(_key!));
|
||||
}
|
||||
if (parts.isEmpty) return translate('shortcut-recording-press-keys-tip');
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
String _keyDisplay(String key) {
|
||||
switch (key) {
|
||||
case 'delete': return 'Del';
|
||||
case 'backspace': return 'Backspace';
|
||||
case 'enter': return 'Enter';
|
||||
case 'tab': return 'Tab';
|
||||
case 'space': return 'Space';
|
||||
case 'arrow_left': return 'Left';
|
||||
case 'arrow_right':return 'Right';
|
||||
case 'arrow_up': return 'Up';
|
||||
case 'arrow_down': return 'Down';
|
||||
case 'home': return 'Home';
|
||||
case 'end': return 'End';
|
||||
case 'page_up': return 'PgUp';
|
||||
case 'page_down': return 'PgDn';
|
||||
case 'insert': return 'Ins';
|
||||
}
|
||||
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;
|
||||
// The Save button still fires for the previously-captured combo even if
|
||||
// the user just hit an unsupported key — the captured state is what gets
|
||||
// saved, the warning is just feedback that the latest press was rejected.
|
||||
final canSave = hasKey && _hasRequiredPrefix;
|
||||
|
||||
Widget statusLine;
|
||||
if (_unsupportedKey != null) {
|
||||
// Most recent press was unsupported. Take precedence over the
|
||||
// captured-combo states so the user gets explicit feedback that their
|
||||
// last keystroke was ignored, regardless of whether a previous combo
|
||||
// is still captured.
|
||||
statusLine = Row(
|
||||
children: [
|
||||
const Icon(Icons.close, size: 16, color: Colors.red),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
translate('shortcut-key-not-supported')
|
||||
.replaceAll('{}', _unsupportedKey!),
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else 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-modifiers')
|
||||
.replaceAll('{}', _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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import 'shortcut_constants.dart';
|
||||
import 'shortcut_utils.dart';
|
||||
|
||||
/// Marker for the union of [KeyboardShortcutActionEntry] /
|
||||
/// [KeyboardShortcutActionSubgroup] — anything a top-level
|
||||
/// [KeyboardShortcutActionGroup] can directly contain. Sealed so renderers
|
||||
/// and filters can `switch` on it without a default branch.
|
||||
sealed class KeyboardShortcutActionGroupChild {
|
||||
const KeyboardShortcutActionGroupChild();
|
||||
}
|
||||
|
||||
/// One configurable action — id + i18n key for its label.
|
||||
class KeyboardShortcutActionEntry extends KeyboardShortcutActionGroupChild {
|
||||
final String id;
|
||||
final String labelKey;
|
||||
const KeyboardShortcutActionEntry(this.id, this.labelKey);
|
||||
}
|
||||
|
||||
/// A nested subgroup (e.g. "View Mode" under "Display"). Renders with extra
|
||||
/// indent so its items are visually distinguished from the parent group's
|
||||
/// direct items.
|
||||
class KeyboardShortcutActionSubgroup extends KeyboardShortcutActionGroupChild {
|
||||
final String titleKey;
|
||||
final List<KeyboardShortcutActionEntry> entries;
|
||||
const KeyboardShortcutActionSubgroup(this.titleKey, this.entries);
|
||||
}
|
||||
|
||||
/// A top-level group ("Display", "Keyboard", "Chat", …). `children` is an
|
||||
/// *ordered* mix of direct entries and subgroups, so layouts like
|
||||
/// "subgroups first → direct items → trailing subgroup" — exactly the
|
||||
/// shape `_DisplayMenu` uses (Privacy mode lives after the cursor / display
|
||||
/// toggles direct items) — are first-class instead of needing a wrapper
|
||||
/// "Display Settings" subgroup just to insert the items.
|
||||
class KeyboardShortcutActionGroup {
|
||||
final String titleKey;
|
||||
final List<KeyboardShortcutActionGroupChild> children;
|
||||
const KeyboardShortcutActionGroup(this.titleKey, this.children);
|
||||
}
|
||||
|
||||
/// Canonical action group definitions used by both the desktop and mobile
|
||||
/// configuration pages. The order of groups, subgroups, and entries here
|
||||
/// is the order the user sees in the UI, and mirrors the corresponding
|
||||
/// toolbar submenu (`_DisplayMenu` / `_KeyboardMenu` in
|
||||
/// `desktop/widgets/remote_toolbar.dart`) child order — modulo entries
|
||||
/// without shortcut counterparts (e.g. `_screenAdjustor.adjustWindow`,
|
||||
/// `scrollStyle`, `_ResolutionsMenu`, `localKeyboardType`).
|
||||
final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
|
||||
KeyboardShortcutActionGroup('Monitor', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSwitchDisplayNext, 'Switch to next display'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSwitchDisplayPrev, 'Switch to previous display'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSwitchDisplayAll, 'All monitors'),
|
||||
]),
|
||||
KeyboardShortcutActionGroup('Control Actions', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSendClipboardKeystrokes, 'Send clipboard keystrokes'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionResetCanvas, 'Reset canvas'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSendCtrlAltDel, 'Insert Ctrl + Alt + Del'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionRestartRemote, 'Restart remote device'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionInsertLock, 'Insert Lock'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleBlockInput, 'Block user input'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionSwitchSides, 'Switch Sides'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionRefresh, 'Refresh'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleRecording, 'Toggle session recording'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionScreenshot, 'Take screenshot'),
|
||||
]),
|
||||
// Display: subgroups (View Mode → Image Quality → Codec → Virtual display)
|
||||
// first, then the direct items (cursor toggles + display toggles), then
|
||||
// Privacy mode subgroup last — matching `_DisplayMenu.menuChildrenGetter`
|
||||
// exactly. Rebalancing this order should also rebalance the toolbar.
|
||||
KeyboardShortcutActionGroup('Display', [
|
||||
KeyboardShortcutActionSubgroup('View Mode', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionViewModeOriginal, 'Scale original'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionViewModeAdaptive, 'Scale adaptive'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionViewModeCustom, 'Scale custom'),
|
||||
]),
|
||||
KeyboardShortcutActionSubgroup('Image Quality', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionImageQualityBest, 'Good image quality'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionImageQualityBalanced, 'Balanced'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionImageQualityLow, 'Optimize reaction time'),
|
||||
]),
|
||||
KeyboardShortcutActionSubgroup('Codec', [
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecAuto, 'Auto'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecVp8, 'VP8'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecVp9, 'VP9'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecAv1, 'AV1'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecH264, 'H264'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCodecH265, 'H265'),
|
||||
]),
|
||||
KeyboardShortcutActionSubgroup('Virtual display', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionPlugOutAllVirtualDisplays, 'Plug out all'),
|
||||
]),
|
||||
// Direct items: cursorToggles + display toggles, in toolbar order.
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleShowRemoteCursor, 'Show remote cursor'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleFollowRemoteCursor, 'Follow remote cursor'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleFollowRemoteWindow, 'Follow remote window focus'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleZoomCursor, 'Zoom cursor'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleQualityMonitor, 'Show quality monitor'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionToggleMute, 'Mute'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleEnableFileCopyPaste, 'Enable file copy and paste'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleDisableClipboard, 'Disable clipboard'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleLockAfterSessionEnd, 'Lock after session end'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleTrueColor, 'True color (4:4:4)'),
|
||||
// Privacy mode at the bottom — mirrors `_DisplayMenu` where it's the
|
||||
// last submenu added (line ~1023 of remote_toolbar.dart, after toggles).
|
||||
KeyboardShortcutActionSubgroup('Privacy mode', [
|
||||
// Reuse toolbar's existing impl-name i18n keys. The handler at
|
||||
// runtime matches `privacy_mode_impl_mag_tip` /
|
||||
// `privacy_mode_impl_virtual_display_tip` against the peer's
|
||||
// advertised impls — same logic the toolbar's `toolbarPrivacyMode`
|
||||
// submenu uses.
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionPrivacyMode1, 'privacy_mode_impl_mag_tip'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionPrivacyMode2, 'privacy_mode_impl_virtual_display_tip'),
|
||||
]),
|
||||
]),
|
||||
// Keyboard: Keyboard mode subgroup first, then direct items
|
||||
// (inputSource → viewMode → showMyCursor → toolbarKeyboardToggles),
|
||||
// matching `_KeyboardMenu.menuChildrenGetter`.
|
||||
KeyboardShortcutActionGroup('Keyboard', [
|
||||
KeyboardShortcutActionSubgroup('Keyboard mode', [
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionKeyboardModeLegacy, 'Legacy mode'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionKeyboardModeMap, 'Map mode'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionKeyboardModeTranslate, 'Translate mode'),
|
||||
]),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleInputSource, 'Toggle input source'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionToggleViewOnly, 'View Mode'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleShowMyCursor, 'Show my cursor'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleSwapCtrlCmd, 'Swap control-command key'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleRelativeMouseMode, 'Relative mouse mode'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleReverseMouseWheel, 'Reverse mouse wheel'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleSwapLeftRightMouse, 'swap-left-right-mouse'),
|
||||
]),
|
||||
KeyboardShortcutActionGroup('Chat', [
|
||||
KeyboardShortcutActionEntry(kShortcutActionToggleChat, 'Text chat'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionToggleVoiceCall, 'Voice call'),
|
||||
]),
|
||||
// "Other" collects single-icon toolbar buttons that have no dropdown
|
||||
// (Pin, Close), plus actions with no toolbar entry at all (Fullscreen —
|
||||
// driven by callback, not menu; Toggle Toolbar / tab navigation — tab
|
||||
// right-click menu, not toolbar). Combined into one group rather than
|
||||
// several 1-item groups for cleaner visual hierarchy.
|
||||
KeyboardShortcutActionGroup('Other', [
|
||||
KeyboardShortcutActionEntry(kShortcutActionPinToolbar, 'Pin Toolbar'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionToggleFullscreen, 'Toggle fullscreen'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionToggleToolbar, 'Toggle toolbar'),
|
||||
KeyboardShortcutActionEntry(kShortcutActionCloseTab, 'Close tab'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSwitchTabNext, 'Switch to next tab'),
|
||||
KeyboardShortcutActionEntry(
|
||||
kShortcutActionSwitchTabPrev, 'Switch to previous tab'),
|
||||
]),
|
||||
];
|
||||
|
||||
/// Walk the (filtered or unfiltered) group tree and yield every
|
||||
/// [KeyboardShortcutActionEntry], regardless of whether it sits as a direct
|
||||
/// child of a top-level group or inside a subgroup. Useful for label
|
||||
/// lookups, ghost-action tests, and any consumer that just wants the flat
|
||||
/// list of action ids.
|
||||
Iterable<KeyboardShortcutActionEntry> allActionEntries(
|
||||
Iterable<KeyboardShortcutActionGroup> groups,
|
||||
) sync* {
|
||||
for (final group in groups) {
|
||||
for (final child in group.children) {
|
||||
switch (child) {
|
||||
case KeyboardShortcutActionEntry():
|
||||
yield child;
|
||||
case KeyboardShortcutActionSubgroup():
|
||||
yield* child.entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return [kKeyboardShortcutActionGroups] with actions that aren't supported
|
||||
/// on the current platform stripped out. Subgroups whose every entry was
|
||||
/// filtered are dropped; top-level groups whose every child (direct entry
|
||||
/// or subgroup) was dropped are themselves dropped.
|
||||
///
|
||||
/// Mirrors the capability flags used by [filterDefaultBindingsForPlatform]
|
||||
/// so the configuration UI shows only what the matcher can actually
|
||||
/// dispatch on this platform.
|
||||
///
|
||||
/// Note: callers should still walk the unfiltered
|
||||
/// [kKeyboardShortcutActionGroups] for label lookups (e.g. conflict
|
||||
/// warnings about a stale cross-platform binding), so an action bound on
|
||||
/// desktop and carried over to mobile still has a human-readable name in
|
||||
/// dialogs.
|
||||
List<KeyboardShortcutActionGroup> filterKeyboardShortcutActionGroupsForPlatform(
|
||||
ShortcutPlatformCapabilities cap,
|
||||
) {
|
||||
bool allowed(String id) {
|
||||
if (!cap.includeFullscreenShortcut &&
|
||||
id == kShortcutActionToggleFullscreen) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeScreenshotShortcut && id == kShortcutActionScreenshot) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeScreenshotShortcut &&
|
||||
id == kShortcutActionToggleRelativeMouseMode) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(id)) return false;
|
||||
if (!cap.includeToolbarShortcut && id == kShortcutActionToggleToolbar) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeCloseTabShortcut && id == kShortcutActionCloseTab) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeSwitchSidesShortcut && id == kShortcutActionSwitchSides) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeRecordingShortcut && id == kShortcutActionToggleRecording) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeResetCanvasShortcut && id == kShortcutActionResetCanvas) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includePinToolbarShortcut && id == kShortcutActionPinToolbar) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeViewModeShortcut &&
|
||||
(id == kShortcutActionViewModeOriginal ||
|
||||
id == kShortcutActionViewModeAdaptive ||
|
||||
id == kShortcutActionViewModeCustom)) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeInputSourceShortcut &&
|
||||
id == kShortcutActionToggleInputSource) {
|
||||
return false;
|
||||
}
|
||||
if (!cap.includeVoiceCallShortcut && id == kShortcutActionToggleVoiceCall) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
final out = <KeyboardShortcutActionGroup>[];
|
||||
for (final group in kKeyboardShortcutActionGroups) {
|
||||
final filteredChildren = <KeyboardShortcutActionGroupChild>[];
|
||||
for (final child in group.children) {
|
||||
switch (child) {
|
||||
case KeyboardShortcutActionEntry():
|
||||
if (allowed(child.id)) filteredChildren.add(child);
|
||||
case KeyboardShortcutActionSubgroup():
|
||||
final entries =
|
||||
child.entries.where((e) => allowed(e.id)).toList();
|
||||
if (entries.isNotEmpty) {
|
||||
filteredChildren.add(
|
||||
KeyboardShortcutActionSubgroup(child.titleKey, entries));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filteredChildren.isNotEmpty) {
|
||||
out.add(KeyboardShortcutActionGroup(group.titleKey, filteredChildren));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/// 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 kShortcutActionSwitchDisplayAll = 'switch_display_all';
|
||||
const kShortcutActionScreenshot = 'screenshot';
|
||||
const kShortcutActionInsertLock = 'insert_lock';
|
||||
const kShortcutActionRefresh = 'refresh';
|
||||
const kShortcutActionToggleBlockInput = 'toggle_block_input';
|
||||
const kShortcutActionToggleRecording = 'toggle_recording';
|
||||
const kShortcutActionSwitchSides = 'switch_sides';
|
||||
const kShortcutActionCloseTab = 'close_tab';
|
||||
const kShortcutActionToggleToolbar = 'toggle_toolbar';
|
||||
const kShortcutActionRestartRemote = 'restart_remote';
|
||||
const kShortcutActionResetCanvas = 'reset_canvas';
|
||||
const kShortcutActionSwitchTabNext = 'switch_tab_next';
|
||||
const kShortcutActionSwitchTabPrev = 'switch_tab_prev';
|
||||
const kShortcutActionToggleMute = 'toggle_mute';
|
||||
const kShortcutActionPinToolbar = 'pin_toolbar';
|
||||
const kShortcutActionViewModeOriginal = 'view_mode_original';
|
||||
const kShortcutActionViewModeAdaptive = 'view_mode_adaptive';
|
||||
const kShortcutActionToggleChat = 'toggle_chat';
|
||||
const kShortcutActionToggleQualityMonitor = 'toggle_quality_monitor';
|
||||
const kShortcutActionToggleShowRemoteCursor = 'toggle_show_remote_cursor';
|
||||
const kShortcutActionToggleShowMyCursor = 'toggle_show_my_cursor';
|
||||
const kShortcutActionToggleDisableClipboard = 'toggle_disable_clipboard';
|
||||
const kShortcutActionPrivacyMode1 = 'privacy_mode_1';
|
||||
const kShortcutActionPrivacyMode2 = 'privacy_mode_2';
|
||||
// Keyboard mode (Map / Translate / Legacy).
|
||||
const kShortcutActionKeyboardModeMap = 'keyboard_mode_map';
|
||||
const kShortcutActionKeyboardModeTranslate = 'keyboard_mode_translate';
|
||||
const kShortcutActionKeyboardModeLegacy = 'keyboard_mode_legacy';
|
||||
// Codec preference (Auto + the four optional codecs the toolbar surfaces).
|
||||
const kShortcutActionCodecAuto = 'codec_auto';
|
||||
const kShortcutActionCodecVp8 = 'codec_vp8';
|
||||
const kShortcutActionCodecVp9 = 'codec_vp9';
|
||||
const kShortcutActionCodecAv1 = 'codec_av1';
|
||||
const kShortcutActionCodecH264 = 'codec_h264';
|
||||
const kShortcutActionCodecH265 = 'codec_h265';
|
||||
// Plug out every virtual display in one shot — toolbar exposes this in
|
||||
// both IDD modes (RustDesk and Amyuni). Per-index virtual-display toggles
|
||||
// (RustDesk IDD's 4 checkboxes) and the +/- count buttons (Amyuni-only)
|
||||
// are NOT exposed as shortcuts: per-index is too granular, and +/- has
|
||||
// no toolbar counterpart on RustDesk IDD peers.
|
||||
const kShortcutActionPlugOutAllVirtualDisplays =
|
||||
'plug_out_all_virtual_displays';
|
||||
const kShortcutActionToggleRelativeMouseMode = 'toggle_relative_mouse_mode';
|
||||
const kShortcutActionToggleFollowRemoteCursor = 'toggle_follow_remote_cursor';
|
||||
const kShortcutActionToggleFollowRemoteWindow = 'toggle_follow_remote_window';
|
||||
const kShortcutActionToggleZoomCursor = 'toggle_zoom_cursor';
|
||||
const kShortcutActionToggleReverseMouseWheel = 'toggle_reverse_mouse_wheel';
|
||||
const kShortcutActionToggleSwapLeftRightMouse = 'toggle_swap_left_right_mouse';
|
||||
const kShortcutActionToggleLockAfterSessionEnd = 'toggle_lock_after_session_end';
|
||||
const kShortcutActionToggleTrueColor = 'toggle_true_color';
|
||||
const kShortcutActionToggleSwapCtrlCmd = 'toggle_swap_ctrl_cmd';
|
||||
const kShortcutActionToggleEnableFileCopyPaste = 'toggle_enable_file_copy_paste';
|
||||
const kShortcutActionViewModeCustom = 'view_mode_custom';
|
||||
const kShortcutActionImageQualityBest = 'image_quality_best';
|
||||
const kShortcutActionImageQualityBalanced = 'image_quality_balanced';
|
||||
const kShortcutActionImageQualityLow = 'image_quality_low';
|
||||
const kShortcutActionSendClipboardKeystrokes = 'send_clipboard_keystrokes';
|
||||
const kShortcutActionToggleInputSource = 'toggle_input_source';
|
||||
const kShortcutActionToggleVoiceCall = 'toggle_voice_call';
|
||||
const kShortcutActionToggleViewOnly = 'toggle_view_only';
|
||||
|
||||
const kShortcutLocalConfigKey = 'keyboard-shortcuts';
|
||||
const kShortcutEventName = 'shortcut_triggered';
|
||||
|
||||
/// Canonical default keyboard-shortcut bindings, mirroring Rust's
|
||||
/// `default_bindings()` in `src/keyboard/shortcuts.rs`. Used by:
|
||||
/// * the Web bridge (`flutter/lib/web/bridge.dart::mainGetDefaultKeyboardShortcuts`)
|
||||
/// — Web has no Rust at runtime, so the seed list is read from this Dart
|
||||
/// constant instead of going through FFI.
|
||||
/// * the configuration page when seeding defaults on first enable, after
|
||||
/// [filterDefaultBindingsForPlatform] has trimmed platform-specific
|
||||
/// entries.
|
||||
///
|
||||
/// Parity with Rust is unit-tested on both sides against
|
||||
/// `flutter/test/fixtures/default_keyboard_shortcuts.json` — see the
|
||||
/// `kDefaultShortcutBindings matches fixture` test in
|
||||
/// `flutter/test/keyboard_shortcuts_test.dart` and
|
||||
/// `default_bindings_match_fixture_json` in `src/keyboard/shortcuts.rs`.
|
||||
/// Any change here MUST also update the fixture and the Rust source, or CI
|
||||
/// will fail in the side that drifted.
|
||||
final List<Map<String, Object>> kDefaultShortcutBindings = [
|
||||
for (final entry in <List<Object>>[
|
||||
[kShortcutActionSendCtrlAltDel, 'delete'],
|
||||
[kShortcutActionToggleFullscreen, 'enter'],
|
||||
[kShortcutActionSwitchDisplayNext, 'arrow_right'],
|
||||
[kShortcutActionSwitchDisplayPrev, 'arrow_left'],
|
||||
[kShortcutActionScreenshot, 'p'],
|
||||
[kShortcutActionToggleShowRemoteCursor, 'm'],
|
||||
[kShortcutActionToggleMute, 's'],
|
||||
[kShortcutActionToggleBlockInput, 'i'],
|
||||
[kShortcutActionToggleChat, 'c'],
|
||||
])
|
||||
{
|
||||
'action': entry[0],
|
||||
'mods': const ['primary', 'alt', 'shift'],
|
||||
'key': entry[1],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,226 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'shortcut_constants.dart';
|
||||
|
||||
List<String> canonicalShortcutModsForSave(Set<String> mods) {
|
||||
return <String>[
|
||||
if (mods.contains('primary')) 'primary',
|
||||
if (mods.contains('ctrl')) 'ctrl',
|
||||
if (mods.contains('alt')) 'alt',
|
||||
if (mods.contains('shift')) 'shift',
|
||||
];
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> shortcutBindingMapsFrom(dynamic rawBindings) {
|
||||
if (rawBindings is! Iterable) return <Map<String, dynamic>>[];
|
||||
final bindings = <Map<String, dynamic>>[];
|
||||
for (final raw in rawBindings) {
|
||||
if (raw is! Map) continue;
|
||||
final binding = <String, dynamic>{};
|
||||
for (final entry in raw.entries) {
|
||||
final key = entry.key;
|
||||
if (key is String) {
|
||||
binding[key] = entry.value;
|
||||
}
|
||||
}
|
||||
if (binding.isNotEmpty) {
|
||||
bindings.add(binding);
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
Set<String> shortcutModSetFrom(dynamic rawMods) {
|
||||
if (rawMods is! Iterable) return <String>{};
|
||||
return rawMods.whereType<String>().toSet();
|
||||
}
|
||||
|
||||
bool isSwitchTabShortcutAction(String? actionId) {
|
||||
return actionId == kShortcutActionSwitchTabNext ||
|
||||
actionId == kShortcutActionSwitchTabPrev;
|
||||
}
|
||||
|
||||
/// Map a [LogicalKeyboardKey] to the canonical key name used in saved
|
||||
/// bindings, or `null` for keys we don't accept as shortcuts.
|
||||
///
|
||||
/// 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. Cross-language parity is enforced by:
|
||||
/// * `flutter/test/fixtures/supported_shortcut_keys.json` — the
|
||||
/// authoritative list of names this function must produce.
|
||||
/// * Dart `supported keys` test in `keyboard_shortcuts_test.dart` —
|
||||
/// asserts the (LogicalKeyboardKey → name) mapping covers the fixture.
|
||||
/// * Rust `supported_keys_match_fixture` test in `shortcuts.rs` — the
|
||||
/// Rust-side mirror against the same fixture.
|
||||
/// A drift in any of the three breaks one of the two tests.
|
||||
String? logicalKeyName(LogicalKeyboardKey k) {
|
||||
// Singletons that map 1:1.
|
||||
if (k == LogicalKeyboardKey.delete) return 'delete';
|
||||
if (k == LogicalKeyboardKey.backspace) return 'backspace';
|
||||
// Numpad Enter shares the "enter" name with the main Return key — matches
|
||||
// the Rust matcher (`Return | KpReturn` → "enter") and matches user
|
||||
// expectation that the two physical Enters are interchangeable.
|
||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||
return 'enter';
|
||||
}
|
||||
if (k == LogicalKeyboardKey.tab) return 'tab';
|
||||
if (k == LogicalKeyboardKey.space) return 'space';
|
||||
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';
|
||||
if (k == LogicalKeyboardKey.home) return 'home';
|
||||
if (k == LogicalKeyboardKey.end) return 'end';
|
||||
if (k == LogicalKeyboardKey.pageUp) return 'page_up';
|
||||
if (k == LogicalKeyboardKey.pageDown) return 'page_down';
|
||||
if (k == LogicalKeyboardKey.insert) return 'insert';
|
||||
|
||||
// Letter / digit / F-key tables. `LogicalKeyboardKey` constants are
|
||||
// `static final` (not `const`), so the maps can't be `const` — but they
|
||||
// initialize once per process and the lookup is O(1).
|
||||
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',
|
||||
};
|
||||
final letter = letters[k];
|
||||
if (letter != null) return letter;
|
||||
|
||||
final digits = <LogicalKeyboardKey, String>{
|
||||
LogicalKeyboardKey.digit0: 'digit0',
|
||||
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',
|
||||
};
|
||||
final digit = digits[k];
|
||||
if (digit != null) return digit;
|
||||
|
||||
final fkeys = <LogicalKeyboardKey, String>{
|
||||
LogicalKeyboardKey.f1: 'f1', LogicalKeyboardKey.f2: 'f2',
|
||||
LogicalKeyboardKey.f3: 'f3', LogicalKeyboardKey.f4: 'f4',
|
||||
LogicalKeyboardKey.f5: 'f5', LogicalKeyboardKey.f6: 'f6',
|
||||
LogicalKeyboardKey.f7: 'f7', LogicalKeyboardKey.f8: 'f8',
|
||||
LogicalKeyboardKey.f9: 'f9', LogicalKeyboardKey.f10: 'f10',
|
||||
LogicalKeyboardKey.f11: 'f11', LogicalKeyboardKey.f12: 'f12',
|
||||
};
|
||||
return fkeys[k];
|
||||
}
|
||||
|
||||
/// Bundle of "is this shortcut available on the current platform" flags.
|
||||
///
|
||||
/// Production code reaches a single source of truth via
|
||||
/// [ShortcutModel.currentPlatformCapabilities] (which encodes the per-runtime
|
||||
/// rules in one place); tests construct one directly with whichever flags
|
||||
/// they want to exercise. Two filter functions consume this:
|
||||
/// [filterDefaultBindingsForPlatform] (for trimming default-binding JSON
|
||||
/// before it hits LocalConfig) and [filterKeyboardShortcutActionGroupsForPlatform]
|
||||
/// (for trimming the configuration UI's action list). Both must agree on the
|
||||
/// same capability set, otherwise a default binding could be seeded for an
|
||||
/// action the user has no UI to manage.
|
||||
class ShortcutPlatformCapabilities {
|
||||
final bool includeFullscreenShortcut;
|
||||
final bool includeScreenshotShortcut;
|
||||
final bool includeTabShortcuts;
|
||||
final bool includeToolbarShortcut;
|
||||
final bool includeCloseTabShortcut;
|
||||
final bool includeSwitchSidesShortcut;
|
||||
final bool includeRecordingShortcut;
|
||||
final bool includeResetCanvasShortcut;
|
||||
final bool includePinToolbarShortcut;
|
||||
final bool includeViewModeShortcut;
|
||||
final bool includeInputSourceShortcut;
|
||||
final bool includeVoiceCallShortcut;
|
||||
|
||||
const ShortcutPlatformCapabilities({
|
||||
required this.includeFullscreenShortcut,
|
||||
required this.includeScreenshotShortcut,
|
||||
required this.includeTabShortcuts,
|
||||
required this.includeToolbarShortcut,
|
||||
required this.includeCloseTabShortcut,
|
||||
required this.includeSwitchSidesShortcut,
|
||||
required this.includeRecordingShortcut,
|
||||
required this.includeResetCanvasShortcut,
|
||||
required this.includePinToolbarShortcut,
|
||||
required this.includeViewModeShortcut,
|
||||
required this.includeInputSourceShortcut,
|
||||
required this.includeVoiceCallShortcut,
|
||||
});
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
|
||||
Iterable<dynamic> bindings,
|
||||
ShortcutPlatformCapabilities cap,
|
||||
) {
|
||||
final filtered = <Map<String, dynamic>>[];
|
||||
for (final binding in shortcutBindingMapsFrom(bindings)) {
|
||||
final action = binding['action'] as String?;
|
||||
if (!cap.includeFullscreenShortcut &&
|
||||
action == kShortcutActionToggleFullscreen) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeScreenshotShortcut && action == kShortcutActionScreenshot) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeScreenshotShortcut &&
|
||||
action == kShortcutActionToggleRelativeMouseMode) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(action)) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeToolbarShortcut &&
|
||||
action == kShortcutActionToggleToolbar) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeCloseTabShortcut && action == kShortcutActionCloseTab) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeSwitchSidesShortcut &&
|
||||
action == kShortcutActionSwitchSides) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeRecordingShortcut &&
|
||||
action == kShortcutActionToggleRecording) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeResetCanvasShortcut &&
|
||||
action == kShortcutActionResetCanvas) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includePinToolbarShortcut && action == kShortcutActionPinToolbar) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeViewModeShortcut &&
|
||||
(action == kShortcutActionViewModeOriginal ||
|
||||
action == kShortcutActionViewModeAdaptive ||
|
||||
action == kShortcutActionViewModeCustom)) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeInputSourceShortcut &&
|
||||
action == kShortcutActionToggleInputSource) {
|
||||
continue;
|
||||
}
|
||||
if (!cap.includeVoiceCallShortcut &&
|
||||
action == kShortcutActionToggleVoiceCall) {
|
||||
continue;
|
||||
}
|
||||
filtered.add(binding);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,21 +11,83 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/shortcut_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
|
||||
/// Action IDs that `toolbarControls` is the sole registrar for. Wiped on
|
||||
/// every call so stale closures don't outlive the menu entry that owned
|
||||
/// them. Actions registered by `registerSessionShortcutActions` MUST NOT
|
||||
/// appear here. `kShortcutActionToggleRecording` is platform-conditional
|
||||
/// and handled separately in the unregister pass below.
|
||||
const _kToolbarOwnedActionIds = <String>[
|
||||
kShortcutActionSendCtrlAltDel,
|
||||
kShortcutActionRestartRemote,
|
||||
kShortcutActionInsertLock,
|
||||
kShortcutActionToggleBlockInput,
|
||||
kShortcutActionSwitchSides,
|
||||
kShortcutActionRefresh,
|
||||
kShortcutActionScreenshot,
|
||||
kShortcutActionResetCanvas,
|
||||
kShortcutActionSendClipboardKeystrokes,
|
||||
];
|
||||
|
||||
const _kToolbarViewStyleActionIds = <String>[
|
||||
kShortcutActionViewModeOriginal,
|
||||
kShortcutActionViewModeAdaptive,
|
||||
kShortcutActionViewModeCustom,
|
||||
];
|
||||
|
||||
const _kToolbarImageQualityActionIds = <String>[
|
||||
kShortcutActionImageQualityBest,
|
||||
kShortcutActionImageQualityBalanced,
|
||||
kShortcutActionImageQualityLow,
|
||||
];
|
||||
|
||||
const _kToolbarCodecActionIds = <String>[
|
||||
kShortcutActionCodecAuto,
|
||||
kShortcutActionCodecVp8,
|
||||
kShortcutActionCodecVp9,
|
||||
kShortcutActionCodecAv1,
|
||||
kShortcutActionCodecH264,
|
||||
kShortcutActionCodecH265,
|
||||
];
|
||||
|
||||
const _kToolbarCursorActionIds = <String>[
|
||||
kShortcutActionToggleShowRemoteCursor,
|
||||
kShortcutActionToggleFollowRemoteCursor,
|
||||
kShortcutActionToggleFollowRemoteWindow,
|
||||
kShortcutActionToggleZoomCursor,
|
||||
];
|
||||
|
||||
const _kToolbarDisplayToggleActionIds = <String>[
|
||||
kShortcutActionToggleQualityMonitor,
|
||||
kShortcutActionToggleMute,
|
||||
kShortcutActionToggleEnableFileCopyPaste,
|
||||
kShortcutActionToggleDisableClipboard,
|
||||
kShortcutActionToggleLockAfterSessionEnd,
|
||||
kShortcutActionToggleTrueColor,
|
||||
];
|
||||
|
||||
const _kToolbarKeyboardToggleActionIds = <String>[
|
||||
kShortcutActionToggleSwapCtrlCmd,
|
||||
kShortcutActionToggleSwapLeftRightMouse,
|
||||
];
|
||||
|
||||
class TTextMenu {
|
||||
final Widget child;
|
||||
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) {
|
||||
@@ -47,20 +109,73 @@ class TRadioMenu<T> {
|
||||
final T value;
|
||||
final T groupValue;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final String? actionId;
|
||||
|
||||
TRadioMenu(
|
||||
{required this.child,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged});
|
||||
required this.onChanged,
|
||||
this.actionId});
|
||||
}
|
||||
|
||||
class TToggleMenu {
|
||||
final Widget child;
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final String? actionId;
|
||||
TToggleMenu(
|
||||
{required this.child, required this.value, required this.onChanged});
|
||||
{required this.child,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.actionId});
|
||||
}
|
||||
|
||||
/// Register each tagged entry's `onChanged` with the session [ShortcutModel].
|
||||
/// Passthrough — returns [menus] so a caller can wrap `return [...]` directly.
|
||||
List<TToggleMenu> _registerToggleMenuShortcuts(
|
||||
FFI ffi,
|
||||
List<TToggleMenu> menus, {
|
||||
List<String> ownedActionIds = const [],
|
||||
}) {
|
||||
for (final actionId in ownedActionIds) {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
}
|
||||
for (final menu in menus) {
|
||||
final actionId = menu.actionId;
|
||||
if (actionId == null) continue;
|
||||
final onChanged = menu.onChanged;
|
||||
if (onChanged == null) {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
} else {
|
||||
final value = menu.value;
|
||||
ffi.shortcutModel.register(actionId, () => onChanged(!value));
|
||||
}
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
|
||||
/// Radio variant of [_registerToggleMenuShortcuts].
|
||||
List<TRadioMenu<T>> _registerRadioMenuShortcuts<T>(
|
||||
FFI ffi,
|
||||
List<TRadioMenu<T>> menus, {
|
||||
List<String> ownedActionIds = const [],
|
||||
}) {
|
||||
for (final actionId in ownedActionIds) {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
}
|
||||
for (final menu in menus) {
|
||||
final actionId = menu.actionId;
|
||||
if (actionId == null) continue;
|
||||
final onChanged = menu.onChanged;
|
||||
if (onChanged == null) {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
} else {
|
||||
final value = menu.value;
|
||||
ffi.shortcutModel.register(actionId, () => onChanged(value));
|
||||
}
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
|
||||
handleOsPasswordEditIcon(
|
||||
@@ -94,6 +209,17 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
final sessionId = ffi.sessionId;
|
||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||
|
||||
// Wipe stale registrations from previous menu builds before re-registering
|
||||
// below; runs unconditionally so mid-session enable works without reconnect.
|
||||
for (final actionId in _kToolbarOwnedActionIds) {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
}
|
||||
// toggle_recording is mobile-only here; desktop's registration is owned by
|
||||
// `registerSessionShortcutActions` and must not be touched.
|
||||
if (!(isDesktop || isWeb)) {
|
||||
ffi.shortcutModel.unregister(kShortcutActionToggleRecording);
|
||||
}
|
||||
|
||||
List<TTextMenu> v = [];
|
||||
// elevation
|
||||
if (isDefaultConn &&
|
||||
@@ -147,13 +273,15 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
bind.sessionInputString(
|
||||
sessionId: sessionId, value: data.text ?? "");
|
||||
}
|
||||
}));
|
||||
},
|
||||
actionId: kShortcutActionSendClipboardKeystrokes));
|
||||
}
|
||||
// reset canvas
|
||||
if (isDefaultConn && isMobile) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
onPressed: () => ffi.cursorModel.reset(),
|
||||
actionId: kShortcutActionResetCanvas));
|
||||
}
|
||||
|
||||
// https://github.com/rustdesk/rustdesk/pull/9731
|
||||
@@ -229,7 +357,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
|
||||
@@ -242,7 +371,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
TTextMenu(
|
||||
child: Text(translate('Restart remote device')),
|
||||
onPressed: () =>
|
||||
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
|
||||
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager),
|
||||
actionId: kShortcutActionRestartRemote),
|
||||
);
|
||||
}
|
||||
// insertLock
|
||||
@@ -250,7 +380,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 +399,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 +442,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () => ffi.recordingModel.toggle()));
|
||||
onPressed: () => ffi.recordingModel.toggle(),
|
||||
actionId: kShortcutActionToggleRecording));
|
||||
}
|
||||
|
||||
// to-do:
|
||||
@@ -326,6 +460,14 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
onPressed: ffi.ffiModel.timerScreenshot != null
|
||||
? null
|
||||
: () {
|
||||
// Live cooldown check: the menu rebuilds onPressed=null
|
||||
// whenever toolbarControls runs and finds timerScreenshot
|
||||
// != null, but the keyboard-shortcut callback holds onto
|
||||
// the originally-enabled closure across cooldown periods
|
||||
// (toolbarControls only re-runs on menu open). Without
|
||||
// this guard the second shortcut press during the 30s
|
||||
// cooldown still fires sessionTakeScreenshot.
|
||||
if (ffi.ffiModel.timerScreenshot != null) return;
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
msgBox(
|
||||
sessionId,
|
||||
@@ -343,6 +485,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
});
|
||||
}
|
||||
},
|
||||
actionId: kShortcutActionScreenshot,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -353,6 +496,17 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
|
||||
));
|
||||
}
|
||||
// Register tagged TTextMenu callbacks. The else-unregister is defense in
|
||||
// depth for actionIds tagged but missing from `_kToolbarOwnedActionIds`.
|
||||
for (final menu in v) {
|
||||
final actionId = menu.actionId;
|
||||
if (actionId == null) continue;
|
||||
if (menu.onPressed != null) {
|
||||
ffi.shortcutModel.register(actionId, menu.onPressed!);
|
||||
} else {
|
||||
ffi.shortcutModel.unregister(actionId);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -367,23 +521,26 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
||||
.then((_) => ffi.canvasModel.updateViewStyle());
|
||||
}
|
||||
|
||||
return [
|
||||
return _registerRadioMenuShortcuts(ffi, [
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale original')),
|
||||
value: kRemoteViewStyleOriginal,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionViewModeOriginal),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale adaptive')),
|
||||
value: kRemoteViewStyleAdaptive,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionViewModeAdaptive),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale custom')),
|
||||
value: kRemoteViewStyleCustom,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged)
|
||||
];
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionViewModeCustom)
|
||||
], ownedActionIds: _kToolbarViewStyleActionIds);
|
||||
}
|
||||
|
||||
Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
||||
@@ -395,22 +552,25 @@ Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
||||
await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value);
|
||||
}
|
||||
|
||||
return [
|
||||
return _registerRadioMenuShortcuts(ffi, [
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Good image quality')),
|
||||
value: kRemoteImageQualityBest,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionImageQualityBest),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Balanced')),
|
||||
value: kRemoteImageQualityBalanced,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionImageQualityBalanced),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Optimize reaction time')),
|
||||
value: kRemoteImageQualityLow,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
onChanged: onChanged,
|
||||
actionId: kShortcutActionImageQualityLow),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Custom')),
|
||||
value: kRemoteImageQualityCustom,
|
||||
@@ -420,7 +580,7 @@ Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
||||
customImageQualityDialog(ffi.sessionId, id, ffi);
|
||||
},
|
||||
),
|
||||
];
|
||||
], ownedActionIds: _kToolbarImageQualityActionIds);
|
||||
}
|
||||
|
||||
Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
@@ -447,7 +607,10 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
}
|
||||
final visible =
|
||||
codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
|
||||
if (!visible) return [];
|
||||
if (!visible) {
|
||||
return _registerRadioMenuShortcuts<String>(ffi, [],
|
||||
ownedActionIds: _kToolbarCodecActionIds);
|
||||
}
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionPeerOption(
|
||||
@@ -455,12 +618,14 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||
}
|
||||
|
||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||
TRadioMenu<String> radio(
|
||||
String label, String value, bool enabled, String actionId) {
|
||||
return TRadioMenu<String>(
|
||||
child: Text(label),
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? onChanged : null);
|
||||
onChanged: enabled ? onChanged : null,
|
||||
actionId: actionId);
|
||||
}
|
||||
|
||||
var autoLabel = translate('Auto');
|
||||
@@ -468,14 +633,14 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||
}
|
||||
return [
|
||||
radio(autoLabel, 'auto', true),
|
||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||
radio('VP9', 'vp9', true),
|
||||
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
||||
if (codecs[2]) radio('H264', 'h264', codecs[2]),
|
||||
if (codecs[3]) radio('H265', 'h265', codecs[3]),
|
||||
];
|
||||
return _registerRadioMenuShortcuts(ffi, [
|
||||
radio(autoLabel, 'auto', true, kShortcutActionCodecAuto),
|
||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0], kShortcutActionCodecVp8),
|
||||
radio('VP9', 'vp9', true, kShortcutActionCodecVp9),
|
||||
if (codecs[1]) radio('AV1', 'av1', codecs[1], kShortcutActionCodecAv1),
|
||||
if (codecs[2]) radio('H264', 'h264', codecs[2], kShortcutActionCodecH264),
|
||||
if (codecs[3]) radio('H265', 'h265', codecs[3], kShortcutActionCodecH265),
|
||||
], ownedActionIds: _kToolbarCodecActionIds);
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarCursor(
|
||||
@@ -500,6 +665,7 @@ Future<List<TToggleMenu>> toolbarCursor(
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Show remote cursor')),
|
||||
value: state.value,
|
||||
actionId: kShortcutActionToggleShowRemoteCursor,
|
||||
onChanged: enabled && !lockState.value
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
@@ -536,6 +702,7 @@ Future<List<TToggleMenu>> toolbarCursor(
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote cursor')),
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleFollowRemoteCursor,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -564,6 +731,7 @@ Future<List<TToggleMenu>> toolbarCursor(
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote window focus')),
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleFollowRemoteWindow,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -581,6 +749,7 @@ Future<List<TToggleMenu>> toolbarCursor(
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Zoom cursor')),
|
||||
value: peerState.value,
|
||||
actionId: kShortcutActionToggleZoomCursor,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -589,7 +758,8 @@ Future<List<TToggleMenu>> toolbarCursor(
|
||||
},
|
||||
));
|
||||
}
|
||||
return v;
|
||||
return _registerToggleMenuShortcuts(ffi, v,
|
||||
ownedActionIds: _kToolbarCursorActionIds);
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
@@ -605,6 +775,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
final option = 'show-quality-monitor';
|
||||
v.add(TToggleMenu(
|
||||
value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option),
|
||||
actionId: kShortcutActionToggleQualityMonitor,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -618,6 +789,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleMute,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -642,6 +814,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleEnableFileCopyPaste,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
@@ -660,6 +833,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
if (ffiModel.viewOnly) value = true;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleDisableClipboard,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
@@ -676,6 +850,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleLockAfterSessionEnd,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
@@ -726,6 +901,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleTrueColor,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
@@ -750,7 +926,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
child: Text(translate('View Mode'))));
|
||||
}
|
||||
return v;
|
||||
return _registerToggleMenuShortcuts(ffi, v,
|
||||
ownedActionIds: _kToolbarDisplayToggleActionIds);
|
||||
}
|
||||
|
||||
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
|
||||
@@ -849,6 +1026,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleSwapCtrlCmd,
|
||||
onChanged: enabled ? onChanged : null,
|
||||
child: Text(translate('Swap control-command key'))));
|
||||
}
|
||||
@@ -914,10 +1092,27 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
actionId: kShortcutActionToggleSwapLeftRightMouse,
|
||||
onChanged: enabled ? onChanged : null,
|
||||
child: Text(translate('swap-left-right-mouse'))));
|
||||
}
|
||||
return v;
|
||||
return _registerToggleMenuShortcuts(ffi, v,
|
||||
ownedActionIds: _kToolbarKeyboardToggleActionIds);
|
||||
}
|
||||
|
||||
/// Drive each toolbar helper for its registration side effect, so a shortcut
|
||||
/// fires from the first keystroke without needing the user to open the
|
||||
/// matching submenu. Mobile gets `toolbarKeyboardToggles` via
|
||||
/// `toolbarDisplayToggle`'s `isMobile` branch — calling it explicitly there
|
||||
/// would double-register.
|
||||
void registerToolbarShortcuts(BuildContext context, String id, FFI ffi) {
|
||||
if (isDesktop) toolbarKeyboardToggles(ffi);
|
||||
unawaited(toolbarCursor(context, id, ffi));
|
||||
unawaited(toolbarDisplayToggle(context, id, ffi));
|
||||
unawaited(toolbarViewStyle(context, id, ffi));
|
||||
unawaited(toolbarImageQuality(context, id, ffi));
|
||||
unawaited(toolbarCodec(context, id, ffi));
|
||||
toolbarPrivacyMode(PrivacyModeState.find(id), context, id, ffi);
|
||||
}
|
||||
|
||||
bool showVirtualDisplayMenu(FFI ffi) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
export 'common/widgets/keyboard_shortcuts/shortcut_constants.dart';
|
||||
|
||||
const int kMaxVirtualDisplayCount = 4;
|
||||
const int kAllVirtualDisplay = -1;
|
||||
|
||||
@@ -187,6 +189,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";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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) {
|
||||
final foregroundColor =
|
||||
AppBarTheme.of(context).titleTextStyle?.color ?? Colors.white;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(translate('Keyboard Shortcuts')),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(foregroundColor: foregroundColor),
|
||||
onPressed: () =>
|
||||
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
|
||||
icon: const Icon(Icons.restore),
|
||||
label: Text(translate('Reset to defaults')),
|
||||
).marginOnly(right: 12),
|
||||
],
|
||||
),
|
||||
body: KeyboardShortcutsPageBody(
|
||||
key: _bodyKey,
|
||||
compact: true,
|
||||
// Desktop's General settings tab already exposes the Enable +
|
||||
// Pass-through checkboxes (it's the only entry point to this page),
|
||||
// so we hide the duplicates here. Mobile shells keep the default
|
||||
// (true) because their entry tile doesn't carry the toggles.
|
||||
showMasterToggles: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,15 @@ 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/common/widgets/keyboard_shortcuts/page_body.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 +424,50 @@ 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`) holds three
|
||||
// flags + the bindings list: {enabled, pass_through, bindings}. When the
|
||||
// master is off, the pass-through toggle and the Configure entry are
|
||||
// hidden — both are meaningless without an active matcher.
|
||||
return StatefulBuilder(builder: (context, setLocalState) {
|
||||
final enabled = ShortcutModel.isEnabled();
|
||||
return _Card(title: 'Keyboard Shortcuts', children: [
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable keyboard shortcuts in remote session',
|
||||
kShortcutLocalConfigKey,
|
||||
isServer: false,
|
||||
optGetter: ShortcutModel.isEnabled,
|
||||
optSetter: (_, v) async {
|
||||
await ShortcutModel.setEnabled(v);
|
||||
setLocalState(() {});
|
||||
},
|
||||
),
|
||||
if (enabled) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Pass-through to remote',
|
||||
kShortcutLocalConfigKey,
|
||||
isServer: false,
|
||||
optGetter: ShortcutModel.isPassThrough,
|
||||
optSetter: (_, v) async {
|
||||
await ShortcutModel.setPassThrough(v);
|
||||
setLocalState(() {});
|
||||
},
|
||||
trailing: const InfoTooltipIcon(tipKey: 'shortcut-passthrough-tip'),
|
||||
),
|
||||
_ShortcutsConfigureRow(),
|
||||
],
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget theme() {
|
||||
final current = MyTheme.getThemeModePreference().toShortString();
|
||||
onChanged(String value) async {
|
||||
@@ -2492,6 +2534,8 @@ Widget _OptionCheckBox(
|
||||
bool isServer = true,
|
||||
bool Function()? optGetter,
|
||||
Future<void> Function(String, bool)? optSetter,
|
||||
// Optional widget rendered between the label and the trailing space.
|
||||
Widget? trailing,
|
||||
}) {
|
||||
getOpt() => optGetter != null
|
||||
? optGetter()
|
||||
@@ -2535,11 +2579,23 @@ Widget _OptionCheckBox(
|
||||
offstage: !ref.value || checkedIcon == null,
|
||||
child: checkedIcon?.marginOnly(right: 5),
|
||||
),
|
||||
Expanded(
|
||||
// Without `trailing`, keep the original Expanded(Text) layout.
|
||||
if (trailing == null)
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate(label),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
else ...[
|
||||
Flexible(
|
||||
child: Text(
|
||||
translate(label),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
translate(label),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
),
|
||||
),
|
||||
trailing,
|
||||
const Spacer(),
|
||||
],
|
||||
],
|
||||
),
|
||||
).marginOnly(left: _kCheckBoxLeftMargin),
|
||||
@@ -2946,6 +3002,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
|
||||
|
||||
@@ -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,18 @@ 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);
|
||||
registerSessionShortcutActions(_ffi,
|
||||
tabController: widget.tabController,
|
||||
toolbarState: widget.toolbarState);
|
||||
registerToolbarShortcuts(context, widget.id, _ffi);
|
||||
}
|
||||
});
|
||||
_ffi.canvasModel.initializeEdgeScrollFallback(this);
|
||||
_ffi.start(
|
||||
|
||||
@@ -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';
|
||||
@@ -610,8 +611,9 @@ class _MonitorMenu extends StatelessWidget {
|
||||
tooltip: isMulti
|
||||
? ''
|
||||
: isAllMonitors
|
||||
? 'all monitors'
|
||||
: '#${i + 1} monitor',
|
||||
? translate('All monitors')
|
||||
: translate('Monitor #{}')
|
||||
.replaceAll('{}', '${i + 1}'),
|
||||
hMargin: isMulti ? null : 6,
|
||||
vMargin: isMulti ? null : 12,
|
||||
topLevel: false,
|
||||
@@ -763,8 +765,35 @@ 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),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
hint,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
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);
|
||||
|
||||
95
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
Normal file
95
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 will
|
||||
// log a no-handler debug line if a user binds one.
|
||||
registerSessionShortcutActions(gFFI);
|
||||
registerToolbarShortcuts(context, widget.id, 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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
showThemeSettings(gFFI.dialogManager);
|
||||
},
|
||||
),
|
||||
if (!disabledSettings)
|
||||
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 +1371,4 @@ SettingsTile _getPopupDialogRadioEntry({
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@ class GroupModel {
|
||||
}
|
||||
|
||||
reset() async {
|
||||
initialized = false;
|
||||
groupLoadError.value = '';
|
||||
deviceGroups.clear();
|
||||
users.clear();
|
||||
|
||||
@@ -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 (!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,38 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +785,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);
|
||||
@@ -717,6 +849,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 +888,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 +1115,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 +1138,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 +1495,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 +1517,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 +1529,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;
|
||||
}
|
||||
|
||||
38
flutter/lib/models/input_modifier_utils.dart
Normal file
38
flutter/lib/models/input_modifier_utils.dart
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -3925,6 +3933,7 @@ class FFI {
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.callbacksOnFirstImage.clear();
|
||||
shortcutModel.clear();
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
@@ -3932,6 +3941,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);
|
||||
}
|
||||
|
||||
545
flutter/lib/models/shortcut_model.dart
Normal file
545
flutter/lib/models/shortcut_model.dart
Normal file
@@ -0,0 +1,545 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../common/shared_state.dart' show PrivacyModeState;
|
||||
import '../common/widgets/dialog.dart'
|
||||
show desktopTryShowTabAuditDialogCloseCancelled;
|
||||
import '../common/widgets/keyboard_shortcuts/shortcut_utils.dart';
|
||||
import '../consts.dart';
|
||||
import '../desktop/widgets/remote_toolbar.dart' show ToolbarState;
|
||||
import 'chat_model.dart' show VoiceCallStatus;
|
||||
import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController;
|
||||
import '../models/model.dart';
|
||||
import '../models/platform_model.dart';
|
||||
import '../models/state_model.dart';
|
||||
|
||||
typedef ShortcutCallback = FutureOr<void> Function();
|
||||
|
||||
/// 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, ShortcutCallback> _callbacks = {};
|
||||
|
||||
ShortcutModel(this.parent);
|
||||
|
||||
/// Called by toolbar / menu builders to register what to do when the
|
||||
/// matched shortcut fires.
|
||||
void register(String actionId, ShortcutCallback callback) {
|
||||
_callbacks[actionId] = callback;
|
||||
}
|
||||
|
||||
void unregister(String actionId) {
|
||||
_callbacks.remove(actionId);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_callbacks.clear();
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
unawaited(Future.sync(cb).catchError((e, st) {
|
||||
debugPrint(
|
||||
'shortcut_triggered: handler failed for $actionId: $e\n$st');
|
||||
}));
|
||||
} 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>;
|
||||
return shortcutBindingMapsFrom(parsed['bindings']);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
static bool isPassThrough() {
|
||||
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
||||
if (raw.isEmpty) return false;
|
||||
try {
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return parsed['pass_through'] == true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent companion to [isEnabled]: when on, the matchers return early
|
||||
/// and every keystroke flows through to the remote (i.e. all bindings are
|
||||
/// suspended). Stored in the same JSON blob so a single reload refreshes
|
||||
/// both flags on every active matcher.
|
||||
static Future<void> setPassThrough(bool v) async {
|
||||
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
||||
Map<String, dynamic> json = {};
|
||||
if (raw.isNotEmpty) {
|
||||
try {
|
||||
json = jsonDecode(raw) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
json = {};
|
||||
}
|
||||
}
|
||||
json['pass_through'] = v;
|
||||
await bind.mainSetLocalOption(
|
||||
key: kShortcutLocalConfigKey, value: jsonEncode(json));
|
||||
bind.mainReloadKeyboardShortcuts();
|
||||
}
|
||||
|
||||
/// Flip the master `enabled` flag and persist. On the first enable we seed
|
||||
/// the default bindings so common combos work out of the box; otherwise we
|
||||
/// preserve whatever the user already has. Refreshes the matcher cache so
|
||||
/// the change takes effect immediately (Rust on native, JS via the bridge
|
||||
/// on Web).
|
||||
static Future<void> setEnabled(bool v) async {
|
||||
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
||||
Map<String, dynamic> json = {};
|
||||
if (raw.isNotEmpty) {
|
||||
try {
|
||||
json = jsonDecode(raw) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
json = {};
|
||||
}
|
||||
}
|
||||
json['enabled'] = v;
|
||||
final list = shortcutBindingMapsFrom(json['bindings']);
|
||||
if (v && list.isEmpty) {
|
||||
json['bindings'] = filterDefaultBindingsForPlatform(
|
||||
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
|
||||
currentPlatformCapabilities(),
|
||||
);
|
||||
} else {
|
||||
json['bindings'] = list;
|
||||
}
|
||||
await bind.mainSetLocalOption(
|
||||
key: kShortcutLocalConfigKey, value: jsonEncode(json));
|
||||
bind.mainReloadKeyboardShortcuts();
|
||||
}
|
||||
|
||||
/// Single source of truth for the per-platform "is this shortcut applicable"
|
||||
/// decisions. Both [setEnabled]'s default-seeding pass and the configuration
|
||||
/// page's reset / list-rendering paths read from here, so the seed list and
|
||||
/// the visible action list can never disagree on which platform a given
|
||||
/// action belongs to.
|
||||
///
|
||||
/// Capability rationale:
|
||||
/// * Fullscreen / Toolbar / Pin / View Mode: rendered wherever the
|
||||
/// desktop layout applies (native desktop + Web). Native mobile is
|
||||
/// permanently full-screen and doesn't have a desktop-style toolbar.
|
||||
/// * Screenshot / Switch Sides: native desktop only. The Web bridge
|
||||
/// throws UnimplementedError for `sessionTakeScreenshot`; mobile
|
||||
/// toolbars don't surface either action.
|
||||
/// * Tab navigation / Close Tab: only native desktop ships
|
||||
/// `DesktopTabController`; Web's `RemotePage` is invoked without one.
|
||||
/// * Recording: native desktop has the `_RecordMenu` widget +
|
||||
/// `registerSessionShortcutActions` registration; native Android has
|
||||
/// the `toolbarControls` entry; iOS short-circuits inside
|
||||
/// `recordingModel.toggle()`; Web has no implementation.
|
||||
/// * Reset Canvas: only the mobile toolbar builds the menu entry
|
||||
/// (`isDefaultConn && isMobile` in `toolbarControls`).
|
||||
/// * Input Source: Web only ships a single source so toggling is a
|
||||
/// no-op; the toolbar menu hides itself when fewer than 2 sources are
|
||||
/// advertised.
|
||||
/// * Voice Call: Web bridge throws `UnimplementedError` for both
|
||||
/// `sessionRequestVoiceCall` and `sessionCloseVoiceCall`.
|
||||
static ShortcutPlatformCapabilities currentPlatformCapabilities() {
|
||||
final desktopLayout = isDesktop || isWeb;
|
||||
return ShortcutPlatformCapabilities(
|
||||
includeFullscreenShortcut: desktopLayout,
|
||||
includeScreenshotShortcut: isDesktop,
|
||||
includeTabShortcuts: isDesktop,
|
||||
includeToolbarShortcut: desktopLayout,
|
||||
includeCloseTabShortcut: isDesktop,
|
||||
includeSwitchSidesShortcut: isDesktop,
|
||||
includeRecordingShortcut: !isWeb && !isIOS,
|
||||
includeResetCanvasShortcut: isMobile,
|
||||
includePinToolbarShortcut: desktopLayout,
|
||||
includeViewModeShortcut: desktopLayout,
|
||||
includeInputSourceShortcut: !isWeb,
|
||||
includeVoiceCallShortcut: !isWeb,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// We register unconditionally — even when shortcuts are master-disabled —
|
||||
/// because the matcher (Rust + JS) gates dispatch via the `enabled` flag,
|
||||
/// so registered closures are functionally invisible until the user flips
|
||||
/// shortcuts on. This keeps the wiring simple (no rebind callbacks across
|
||||
/// sessions) and lets the user toggle shortcuts mid-session without
|
||||
/// reconnecting.
|
||||
///
|
||||
/// [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,
|
||||
ToolbarState? toolbarState,
|
||||
}) {
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// Note on disposal: every closure registered below captures `ffi` via
|
||||
// closure environment, so the FFI object stays alive for the duration of
|
||||
// the closure's execution — even across awaits, even if the session is
|
||||
// closed mid-execution. We therefore don't add per-closure liveness
|
||||
// guards: a `WeakReference<FFI>` check would never go null while the
|
||||
// closure is on the call stack, and the underlying `bind.session*` /
|
||||
// model setters tolerate stale-session calls (they no-op on torn-down
|
||||
// sessions). ShortcutModel.onTriggered's existing entry guard
|
||||
// (`_callbacks` lookup returning null after disposal) is the actual
|
||||
// liveness gate.
|
||||
|
||||
// Toggle Fullscreen — available wherever the desktop layout renders
|
||||
// (native desktop + every Web browser, since Web uses the desktop
|
||||
// RemotePage). `stateGlobal.setFullscreen` handles native window vs.
|
||||
// browser fullscreen. Native mobile is permanently full-screen, so the
|
||||
// action is intentionally not registered there.
|
||||
if (isDesktop || isWeb) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () {
|
||||
stateGlobal.setFullscreen(!stateGlobal.fullscreen.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle Recording — desktop only here. Mobile already wires this through
|
||||
// `toolbarControls` (which adds a recording entry on `!(isDesktop||isWeb)`),
|
||||
// but the desktop toolbar uses a separate `_RecordMenu` widget that has no
|
||||
// `actionId`. Without this explicit registration a desktop user could bind
|
||||
// Toggle Recording in settings and the press would have no handler.
|
||||
// `recordingModel.toggle()` itself short-circuits on iOS and on sessions
|
||||
// without recording permission.
|
||||
if (isDesktop) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleRecording, () {
|
||||
ffi.recordingModel.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
// Switch Display Next / Prev — requires the peer to have at least 2
|
||||
// displays. From the "All displays" merged view, Next jumps to display 0
|
||||
// and Prev to the last display, so the user can always escape the merged
|
||||
// view via these shortcuts.
|
||||
void switchDisplayBy(int delta) {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
final count = pi.displays.length;
|
||||
if (count <= 1) return;
|
||||
final current = pi.currentDisplay;
|
||||
final int next;
|
||||
if (current == kAllDisplayValue) {
|
||||
next = delta > 0 ? 0 : count - 1;
|
||||
} else {
|
||||
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 to all-monitors view — mirrors the toolbar Monitor menu's
|
||||
// "all monitors" button (only built when peer has >1 display). Not a
|
||||
// toggle: the toolbar button just sets the merged view; another action
|
||||
// (Switch to next/previous display, or another monitor button) takes
|
||||
// you back to a single display.
|
||||
//
|
||||
// Use `openMonitorInTheSameTab(kAllDisplayValue, ...)` rather than calling
|
||||
// `sessionSwitchDisplay` with `[kAllDisplayValue]` directly — the toolbar
|
||||
// path treats `kAllDisplayValue` as a UI sentinel and expands it to the
|
||||
// real display index list (`[0, 1, ...]`) before sending, then updates
|
||||
// local FfiModel state. Sending `[-1]` raw produces a wire value the
|
||||
// remote can't act on and skips the local state update, so the merged
|
||||
// view never engages.
|
||||
ffi.shortcutModel.register(kShortcutActionSwitchDisplayAll, () {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
if (pi.displays.length <= 1) return;
|
||||
if (pi.currentDisplay == kAllDisplayValue) return;
|
||||
openMonitorInTheSameTab(kAllDisplayValue, ffi, pi);
|
||||
});
|
||||
|
||||
// Switch tab next / prev — 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. We
|
||||
// intentionally don't expose positional ("Switch to tab N") shortcuts:
|
||||
// counting tabs in a long list is impractical, and AnyDesk / Chrome
|
||||
// standard practice is to favour next/prev navigation.
|
||||
if (tabController != null) {
|
||||
void switchTabBy(int delta) {
|
||||
final tabs = tabController.state.value.tabs;
|
||||
if (tabs.length <= 1) return;
|
||||
final cur = tabs.indexWhere((t) => t.key == ffi.id);
|
||||
if (cur < 0) return;
|
||||
final next = (cur + delta + tabs.length) % tabs.length;
|
||||
tabController.jumpTo(next);
|
||||
}
|
||||
|
||||
ffi.shortcutModel
|
||||
.register(kShortcutActionSwitchTabNext, () => switchTabBy(1));
|
||||
ffi.shortcutModel
|
||||
.register(kShortcutActionSwitchTabPrev, () => switchTabBy(-1));
|
||||
|
||||
// Close Tab — desktop only. Mirrors the tab right-click "Close" entry,
|
||||
// including the audit-log confirmation dialog so a shortcut close goes
|
||||
// through the same path as a menu close.
|
||||
ffi.shortcutModel.register(kShortcutActionCloseTab, () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: ffi.id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(ffi.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle Toolbar — desktop only. ToolbarState is window/session-scoped,
|
||||
// owned by the RemotePage that hosts this session.
|
||||
if (toolbarState != null) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleToolbar, () {
|
||||
toolbarState.switchHide(sessionId);
|
||||
});
|
||||
ffi.shortcutModel.register(kShortcutActionPinToolbar, () {
|
||||
toolbarState.switchPin();
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle Chat overlay (open/close the chat panel for this session).
|
||||
// _ChatMenu is a standalone toolbar icon — not part of any toolbar
|
||||
// helper that returns a TToggleMenu list — so its handler is wired
|
||||
// here rather than picked up by helper auto-register.
|
||||
ffi.shortcutModel.register(kShortcutActionToggleChat, () {
|
||||
ffi.chatModel.toggleChatOverlay();
|
||||
});
|
||||
|
||||
// Toggle Voice Call — start when idle, hang up when active. Mirrors the
|
||||
// toolbar's `_VoiceCallMenu` state-driven button. Web bridge throws
|
||||
// UnimplementedError on both sessionRequestVoiceCall and
|
||||
// sessionCloseVoiceCall, so we don't register on web.
|
||||
if (!isWeb) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleVoiceCall, () {
|
||||
final status = ffi.chatModel.voiceCallStatus.value;
|
||||
if (status == VoiceCallStatus.connected ||
|
||||
status == VoiceCallStatus.waitingForResponse) {
|
||||
bind.sessionCloseVoiceCall(sessionId: sessionId);
|
||||
} else {
|
||||
bind.sessionRequestVoiceCall(sessionId: sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Inline _KeyboardMenu items + actions with no toolbar TToggleMenu/TRadioMenu ─
|
||||
// The toolbar's TToggleMenu / TRadioMenu helpers (toolbarDisplayToggle,
|
||||
// toolbarCursor, toolbarKeyboardToggles, toolbarCodec, toolbarPrivacyMode,
|
||||
// toolbarViewStyle, toolbarImageQuality) auto-register their tagged entries
|
||||
// from the bottom of each helper. The handlers below cover what those
|
||||
// helpers DON'T own:
|
||||
// * Show my cursor / Keyboard mode (Map/Translate/Legacy) / View Only
|
||||
// (desktop) — built as widgets directly in `_KeyboardMenu`, not as
|
||||
// TToggleMenu lists. (Mobile View Only IS in toolbarDisplayToggle and
|
||||
// auto-registers; the desktop session-start handler below registers
|
||||
// first and the helper's auto-register on mobile takes over after its
|
||||
// unawaited future resolves.)
|
||||
// * Plug out all virtual displays — built in `getVirtualDisplayMenuChildren`
|
||||
// as a MenuButton, not a TToggleMenu.
|
||||
// * Toggle Input Source — cycle action; the toolbar exposes per-source
|
||||
// radios but no single "cycle to next source" entry.
|
||||
|
||||
// Show my cursor — toolbar (`_KeyboardMenu.showMyCursor`) pushes the new
|
||||
// value into FfiModel.setShowMyCursor and auto-enables view-only when the
|
||||
// toggle goes on, so the user can never control the remote with their own
|
||||
// cursor visible.
|
||||
ffi.shortcutModel.register(kShortcutActionToggleShowMyCursor, () async {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionToggleShowMyCursor);
|
||||
final showMyCursor = await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionToggleShowMyCursor) ??
|
||||
false;
|
||||
ffi.ffiModel.setShowMyCursor(showMyCursor);
|
||||
if (showMyCursor && !ffi.ffiModel.viewOnly) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionToggleViewOnly);
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionToggleViewOnly) ??
|
||||
false;
|
||||
ffi.ffiModel.setViewOnly(ffi.id, viewOnly);
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard mode (Map / Translate / Legacy). Mirrors the radio buttons in
|
||||
// `_KeyboardMenu.keyboardMode()` (built as RdoMenuButton, not TRadioMenu).
|
||||
void registerKeyboardMode(String actionId, String mode) {
|
||||
ffi.shortcutModel.register(actionId, () async {
|
||||
await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||
await ffi.inputModel.updateKeyboardMode();
|
||||
});
|
||||
}
|
||||
|
||||
registerKeyboardMode(kShortcutActionKeyboardModeMap, kKeyMapMode);
|
||||
registerKeyboardMode(kShortcutActionKeyboardModeTranslate, kKeyTranslateMode);
|
||||
registerKeyboardMode(kShortcutActionKeyboardModeLegacy, kKeyLegacyMode);
|
||||
|
||||
// Plug out all virtual displays (Windows + IDD only). Mirrors the toolbar's
|
||||
// "Plug out all" button — present in both IDD modes (RustDesk + Amyuni),
|
||||
// built as a MenuButton inside `getVirtualDisplayMenuChildren`.
|
||||
ffi.shortcutModel.register(kShortcutActionPlugOutAllVirtualDisplays, () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false,
|
||||
);
|
||||
});
|
||||
|
||||
// Privacy mode 1 / 2 — fallback handlers for the single-impl and null-impls
|
||||
// branches of `toolbarPrivacyMode`. The multi-impl branch tags each entry
|
||||
// with the matching actionId and `_registerToggleMenuShortcuts` overrides
|
||||
// these closures with the toolbar's own onChanged. But when the peer only
|
||||
// advertises a single impl (older Linux peers, certain platform configs)
|
||||
// toolbarPrivacyMode returns a `getDefaultMenu` entry without an actionId,
|
||||
// so the auto-register pass skips it — these fallbacks are what actually
|
||||
// wire the shortcut in that case.
|
||||
String? findPrivacyImpl(String nameKey) {
|
||||
final impls = ffi.ffiModel.pi
|
||||
.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
|
||||
as List<dynamic>?;
|
||||
if (impls == null) return null;
|
||||
for (final e in impls) {
|
||||
if (e is List && e.length >= 2 && e[1] == nameKey) return e[0] as String;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Match the multi-impl branch of `toolbarPrivacyMode`: turn this impl on iff
|
||||
// the active impl isn't already this one. Comparing `.value == implKey`
|
||||
// (rather than `.value.isEmpty`) means pressing the mode-1 shortcut while
|
||||
// mode 2 is on correctly turns mode 1 ON, instead of misreading the
|
||||
// "any-mode-active" state as "this-mode-active" and toggling OFF.
|
||||
ffi.shortcutModel.register(kShortcutActionPrivacyMode1, () {
|
||||
final implKey = findPrivacyImpl('privacy_mode_impl_mag_tip');
|
||||
if (implKey == null) return;
|
||||
bind.sessionTogglePrivacyMode(
|
||||
sessionId: sessionId,
|
||||
implKey: implKey,
|
||||
on: PrivacyModeState.find(ffi.id).value != implKey,
|
||||
);
|
||||
});
|
||||
ffi.shortcutModel.register(kShortcutActionPrivacyMode2, () {
|
||||
final implKey = findPrivacyImpl('privacy_mode_impl_virtual_display_tip');
|
||||
if (implKey == null) return;
|
||||
bind.sessionTogglePrivacyMode(
|
||||
sessionId: sessionId,
|
||||
implKey: implKey,
|
||||
on: PrivacyModeState.find(ffi.id).value != implKey,
|
||||
);
|
||||
});
|
||||
|
||||
// View Only — desktop toolbar exposes this inline in `_KeyboardMenu.viewMode`
|
||||
// (mobile is in toolbarDisplayToggle and goes through helper auto-register).
|
||||
// Mirrors the desktop callback: toggle + sync FfiModel.viewOnly +
|
||||
// FfiModel.showMyCursor (the toolbar keeps these in step).
|
||||
ffi.shortcutModel.register(kShortcutActionToggleViewOnly, () async {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionToggleViewOnly);
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionToggleViewOnly) ??
|
||||
false;
|
||||
ffi.ffiModel.setViewOnly(ffi.id, viewOnly);
|
||||
final showMyCursor = await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionToggleShowMyCursor) ??
|
||||
false;
|
||||
ffi.ffiModel.setShowMyCursor(showMyCursor);
|
||||
});
|
||||
|
||||
// Toggle Reverse mouse wheel — read current 'Y'/'N' (falling back to user
|
||||
// default), flip, write back.
|
||||
ffi.shortcutModel.register(kShortcutActionToggleReverseMouseWheel, () async {
|
||||
var cur = bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
|
||||
if (cur == '') {
|
||||
cur = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
|
||||
}
|
||||
final next = cur == 'Y' ? 'N' : 'Y';
|
||||
await bind.sessionSetReverseMouseWheel(sessionId: sessionId, value: next);
|
||||
});
|
||||
|
||||
// Toggle Relative mouse mode (gaming mode). Desktop only.
|
||||
if (isDesktop && !isWeb) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleRelativeMouseMode, () {
|
||||
ffi.inputModel.toggleRelativeMouseMode();
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle Input Source — flips between the available keyboard-event capture
|
||||
// backends (e.g. JS vs Flutter on desktop). Mirrors the radio menu in
|
||||
// remote_toolbar.dart::inputSource(); when fewer than 2 sources are
|
||||
// available the menu hides itself, so this handler is a no-op too.
|
||||
// Useful for accessibility: screen-reader users sometimes need to swap
|
||||
// sources to regain control of the local keyboard (discussion #1933).
|
||||
// Web only ships a single source, so we don't register on web.
|
||||
if (!isWeb) {
|
||||
ffi.shortcutModel.register(kShortcutActionToggleInputSource, () async {
|
||||
final raw = bind.mainSupportedInputSource();
|
||||
if (raw.isEmpty) return;
|
||||
final List<dynamic> list;
|
||||
try {
|
||||
list = jsonDecode(raw) as List<dynamic>;
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (list.length < 2) return;
|
||||
final ids = list
|
||||
.map((e) => (e is List && e.isNotEmpty) ? e[0] as String : '')
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
if (ids.length < 2) return;
|
||||
final current = stateGlobal.getInputSource();
|
||||
final idx = ids.indexOf(current);
|
||||
final next = ids[(idx < 0 ? 0 : idx + 1) % ids.length];
|
||||
await stateGlobal.setInputSource(sessionId, next);
|
||||
await ffi.ffiModel.checkDesktopKeyboardMode();
|
||||
await ffi.inputModel.updateKeyboardMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,21 @@ 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', []);
|
||||
}
|
||||
|
||||
// Web has no Rust at runtime, so the defaults seed comes from the
|
||||
// [kDefaultShortcutBindings] canonical in shortcut_constants.dart. Parity
|
||||
// with Rust's `default_bindings()` is enforced by tests on both sides
|
||||
// against `flutter/test/fixtures/default_keyboard_shortcuts.json`.
|
||||
String mainGetDefaultKeyboardShortcuts({dynamic hint}) {
|
||||
return jsonEncode(kDefaultShortcutBindings);
|
||||
}
|
||||
|
||||
String mainGetInputSource({dynamic hint}) {
|
||||
final inputSource =
|
||||
js.context.callMethod('getByName', ['option:local', 'input-source']);
|
||||
@@ -1176,6 +1192,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 +1563,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})
|
||||
]));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
11
flutter/test/fixtures/default_keyboard_shortcuts.json
vendored
Normal file
11
flutter/test/fixtures/default_keyboard_shortcuts.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"action": "send_ctrl_alt_del", "mods": ["primary", "alt", "shift"], "key": "delete"},
|
||||
{"action": "toggle_fullscreen", "mods": ["primary", "alt", "shift"], "key": "enter"},
|
||||
{"action": "switch_display_next", "mods": ["primary", "alt", "shift"], "key": "arrow_right"},
|
||||
{"action": "switch_display_prev", "mods": ["primary", "alt", "shift"], "key": "arrow_left"},
|
||||
{"action": "screenshot", "mods": ["primary", "alt", "shift"], "key": "p"},
|
||||
{"action": "toggle_show_remote_cursor", "mods": ["primary", "alt", "shift"], "key": "m"},
|
||||
{"action": "toggle_mute", "mods": ["primary", "alt", "shift"], "key": "s"},
|
||||
{"action": "toggle_block_input", "mods": ["primary", "alt", "shift"], "key": "i"},
|
||||
{"action": "toggle_chat", "mods": ["primary", "alt", "shift"], "key": "c"}
|
||||
]
|
||||
11
flutter/test/fixtures/supported_shortcut_keys.json
vendored
Normal file
11
flutter/test/fixtures/supported_shortcut_keys.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
|
||||
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
"digit0", "digit1", "digit2", "digit3", "digit4",
|
||||
"digit5", "digit6", "digit7", "digit8", "digit9",
|
||||
"f1", "f2", "f3", "f4", "f5", "f6",
|
||||
"f7", "f8", "f9", "f10", "f11", "f12",
|
||||
"delete", "backspace", "tab", "space", "enter",
|
||||
"arrow_left", "arrow_right", "arrow_up", "arrow_down",
|
||||
"home", "end", "page_up", "page_down", "insert"
|
||||
]
|
||||
125
flutter/test/input_modifier_utils_test.dart
Normal file
125
flutter/test/input_modifier_utils_test.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
465
flutter/test/keyboard_shortcuts_test.dart
Normal file
465
flutter/test/keyboard_shortcuts_test.dart
Normal file
@@ -0,0 +1,465 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_actions.dart';
|
||||
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_constants.dart';
|
||||
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_utils.dart';
|
||||
|
||||
ShortcutPlatformCapabilities capabilities({
|
||||
bool includeFullscreenShortcut = true,
|
||||
bool includeScreenshotShortcut = true,
|
||||
bool includeTabShortcuts = true,
|
||||
bool includeToolbarShortcut = true,
|
||||
bool includeCloseTabShortcut = true,
|
||||
bool includeSwitchSidesShortcut = true,
|
||||
bool includeRecordingShortcut = true,
|
||||
bool includeResetCanvasShortcut = true,
|
||||
bool includePinToolbarShortcut = true,
|
||||
bool includeViewModeShortcut = true,
|
||||
bool includeInputSourceShortcut = true,
|
||||
bool includeVoiceCallShortcut = true,
|
||||
}) {
|
||||
return ShortcutPlatformCapabilities(
|
||||
includeFullscreenShortcut: includeFullscreenShortcut,
|
||||
includeScreenshotShortcut: includeScreenshotShortcut,
|
||||
includeTabShortcuts: includeTabShortcuts,
|
||||
includeToolbarShortcut: includeToolbarShortcut,
|
||||
includeCloseTabShortcut: includeCloseTabShortcut,
|
||||
includeSwitchSidesShortcut: includeSwitchSidesShortcut,
|
||||
includeRecordingShortcut: includeRecordingShortcut,
|
||||
includeResetCanvasShortcut: includeResetCanvasShortcut,
|
||||
includePinToolbarShortcut: includePinToolbarShortcut,
|
||||
includeViewModeShortcut: includeViewModeShortcut,
|
||||
includeInputSourceShortcut: includeInputSourceShortcut,
|
||||
includeVoiceCallShortcut: includeVoiceCallShortcut,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('kDefaultShortcutBindings matches fixture', () {
|
||||
// The fixture is the cross-language source of truth for default
|
||||
// bindings. Rust has its own parity test against the same file
|
||||
// (`default_bindings_match_fixture_json` in src/keyboard/shortcuts.rs),
|
||||
// so a drift on either side breaks CI.
|
||||
final fixturePath = 'test/fixtures/default_keyboard_shortcuts.json';
|
||||
final fixture =
|
||||
jsonDecode(File(fixturePath).readAsStringSync()) as List<dynamic>;
|
||||
expect(kDefaultShortcutBindings, equals(fixture),
|
||||
reason: 'kDefaultShortcutBindings drifted from $fixturePath — update '
|
||||
'shortcut_constants.dart, the fixture, and Rust default_bindings() '
|
||||
'together');
|
||||
});
|
||||
|
||||
test('save order preserves macOS control modifier', () {
|
||||
expect(canonicalShortcutModsForSave({'ctrl'}), ['ctrl']);
|
||||
expect(canonicalShortcutModsForSave({'shift', 'ctrl', 'primary', 'alt'}),
|
||||
['primary', 'ctrl', 'alt', 'shift']);
|
||||
});
|
||||
|
||||
test('shortcutBindingMapsFrom ignores malformed bindings', () {
|
||||
expect(shortcutBindingMapsFrom('not a list'), isEmpty);
|
||||
|
||||
final bindings = shortcutBindingMapsFrom([
|
||||
{
|
||||
'action': kShortcutActionScreenshot,
|
||||
'mods': ['primary'],
|
||||
'key': 'p',
|
||||
},
|
||||
'bad',
|
||||
1,
|
||||
{
|
||||
'action': kShortcutActionToggleMute,
|
||||
'mods': ['alt'],
|
||||
'key': 's',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(bindings, hasLength(2));
|
||||
expect(bindings.map((binding) => binding['action']), [
|
||||
kShortcutActionScreenshot,
|
||||
kShortcutActionToggleMute,
|
||||
]);
|
||||
});
|
||||
|
||||
test('shortcutModSetFrom ignores malformed modifiers', () {
|
||||
expect(shortcutModSetFrom('not a list'), isEmpty);
|
||||
expect(shortcutModSetFrom(['primary', 1, 'alt', null, 'primary']), {
|
||||
'primary',
|
||||
'alt',
|
||||
});
|
||||
});
|
||||
|
||||
test('non-desktop defaults exclude desktop-only and tab shortcuts', () {
|
||||
final defaults = [
|
||||
{
|
||||
'action': kShortcutActionSendCtrlAltDel,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'delete',
|
||||
},
|
||||
{
|
||||
'action': kShortcutActionToggleFullscreen,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'enter',
|
||||
},
|
||||
{
|
||||
'action': kShortcutActionSwitchDisplayNext,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'arrow_right',
|
||||
},
|
||||
{
|
||||
'action': kShortcutActionScreenshot,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'p',
|
||||
},
|
||||
{
|
||||
'action': kShortcutActionSwitchTabNext,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'right_bracket',
|
||||
},
|
||||
{
|
||||
'action': kShortcutActionToggleRelativeMouseMode,
|
||||
'mods': ['primary', 'alt', 'shift'],
|
||||
'key': 'g',
|
||||
},
|
||||
];
|
||||
|
||||
final filtered = filterDefaultBindingsForPlatform(
|
||||
defaults,
|
||||
capabilities(
|
||||
includeFullscreenShortcut: false,
|
||||
includeScreenshotShortcut: false,
|
||||
includeTabShortcuts: false,
|
||||
includeToolbarShortcut: false,
|
||||
includeCloseTabShortcut: false,
|
||||
includeSwitchSidesShortcut: false,
|
||||
includeRecordingShortcut: false,
|
||||
includeResetCanvasShortcut: false,
|
||||
includePinToolbarShortcut: false,
|
||||
includeViewModeShortcut: false,
|
||||
includeInputSourceShortcut: false,
|
||||
includeVoiceCallShortcut: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(filtered.map((binding) => binding['action']), [
|
||||
kShortcutActionSendCtrlAltDel,
|
||||
kShortcutActionSwitchDisplayNext,
|
||||
]);
|
||||
});
|
||||
|
||||
Set<String> idSet(Iterable<KeyboardShortcutActionGroup> groups) =>
|
||||
{for (final e in allActionEntries(groups)) e.id};
|
||||
|
||||
/// Convenience: extract the children of the named group as a flat list of
|
||||
/// human-readable tokens. Subgroups appear as `'group:<title>'` followed
|
||||
/// by their entries, so call sites can assert on full ordering (subgroups
|
||||
/// interleaved with direct items) in one expectation.
|
||||
List<String> childTokens(
|
||||
List<KeyboardShortcutActionGroup> groups, String titleKey) {
|
||||
final group = groups.firstWhere((g) => g.titleKey == titleKey);
|
||||
final out = <String>[];
|
||||
for (final child in group.children) {
|
||||
switch (child) {
|
||||
case KeyboardShortcutActionEntry():
|
||||
out.add(child.id);
|
||||
case KeyboardShortcutActionSubgroup():
|
||||
out.add('group:${child.titleKey}');
|
||||
for (final entry in child.entries) {
|
||||
out.add(' ${entry.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('filterKeyboardShortcutActionGroupsForPlatform strips desktop-only', () {
|
||||
final groups = filterKeyboardShortcutActionGroupsForPlatform(
|
||||
capabilities(
|
||||
includeFullscreenShortcut: false,
|
||||
includeScreenshotShortcut: false,
|
||||
includeTabShortcuts: false,
|
||||
includeToolbarShortcut: false,
|
||||
includeCloseTabShortcut: false,
|
||||
includeSwitchSidesShortcut: false,
|
||||
// Recording / Reset Canvas are intentionally still included here —
|
||||
// they have non-desktop platforms (mobile Android / mobile both).
|
||||
includeRecordingShortcut: true,
|
||||
includeResetCanvasShortcut: true,
|
||||
includePinToolbarShortcut: false,
|
||||
includeViewModeShortcut: false,
|
||||
includeInputSourceShortcut: false,
|
||||
includeVoiceCallShortcut: false,
|
||||
),
|
||||
);
|
||||
final ids = idSet(groups);
|
||||
// Desktop-only actions are stripped.
|
||||
expect(ids, isNot(contains(kShortcutActionToggleFullscreen)));
|
||||
expect(ids, isNot(contains(kShortcutActionToggleRelativeMouseMode)));
|
||||
expect(ids, isNot(contains(kShortcutActionScreenshot)));
|
||||
expect(ids, isNot(contains(kShortcutActionToggleToolbar)));
|
||||
expect(ids, isNot(contains(kShortcutActionCloseTab)));
|
||||
expect(ids, isNot(contains(kShortcutActionSwitchSides)));
|
||||
expect(ids, isNot(contains(kShortcutActionPinToolbar)));
|
||||
expect(ids, isNot(contains(kShortcutActionViewModeOriginal)));
|
||||
expect(ids, isNot(contains(kShortcutActionViewModeAdaptive)));
|
||||
expect(ids, isNot(contains(kShortcutActionSwitchTabNext)));
|
||||
expect(ids, isNot(contains(kShortcutActionSwitchTabPrev)));
|
||||
// Cross-platform actions survive.
|
||||
expect(ids, contains(kShortcutActionSendCtrlAltDel));
|
||||
expect(ids, contains(kShortcutActionInsertLock));
|
||||
expect(ids, contains(kShortcutActionRestartRemote));
|
||||
expect(ids, contains(kShortcutActionSwitchDisplayNext));
|
||||
expect(ids, contains(kShortcutActionToggleRecording));
|
||||
expect(ids, contains(kShortcutActionResetCanvas));
|
||||
expect(ids, contains(kShortcutActionToggleMute));
|
||||
});
|
||||
|
||||
test(
|
||||
'filterKeyboardShortcutActionGroupsForPlatform hides Toggle Recording on Web/iOS',
|
||||
() {
|
||||
final groups = filterKeyboardShortcutActionGroupsForPlatform(
|
||||
capabilities(includeRecordingShortcut: false),
|
||||
);
|
||||
final ids = idSet(groups);
|
||||
expect(ids, isNot(contains(kShortcutActionToggleRecording)));
|
||||
// Other Session Control entries unaffected.
|
||||
expect(ids, contains(kShortcutActionSendCtrlAltDel));
|
||||
expect(ids, contains(kShortcutActionInsertLock));
|
||||
});
|
||||
|
||||
test(
|
||||
'filterKeyboardShortcutActionGroupsForPlatform keeps full set on desktop',
|
||||
() {
|
||||
final groups =
|
||||
filterKeyboardShortcutActionGroupsForPlatform(capabilities());
|
||||
expect(idSet(groups), equals(idSet(kKeyboardShortcutActionGroups)));
|
||||
});
|
||||
|
||||
test('shortcut action groups follow toolbar menu order', () {
|
||||
final groups = kKeyboardShortcutActionGroups;
|
||||
|
||||
// Top-level groups in toolbar order.
|
||||
expect(
|
||||
groups.map((g) => g.titleKey).toList(),
|
||||
['Monitor', 'Control Actions', 'Display', 'Keyboard', 'Chat', 'Other'],
|
||||
);
|
||||
|
||||
// Display: subgroups (View Mode → Image Quality → Codec → Virtual
|
||||
// display) first, then direct items (cursor toggles + display toggles),
|
||||
// then Privacy mode subgroup last — exactly matching `_DisplayMenu`.
|
||||
expect(childTokens(groups, 'Display'), [
|
||||
'group:View Mode',
|
||||
' $kShortcutActionViewModeOriginal',
|
||||
' $kShortcutActionViewModeAdaptive',
|
||||
' $kShortcutActionViewModeCustom',
|
||||
'group:Image Quality',
|
||||
' $kShortcutActionImageQualityBest',
|
||||
' $kShortcutActionImageQualityBalanced',
|
||||
' $kShortcutActionImageQualityLow',
|
||||
'group:Codec',
|
||||
' $kShortcutActionCodecAuto',
|
||||
' $kShortcutActionCodecVp8',
|
||||
' $kShortcutActionCodecVp9',
|
||||
' $kShortcutActionCodecAv1',
|
||||
' $kShortcutActionCodecH264',
|
||||
' $kShortcutActionCodecH265',
|
||||
'group:Virtual display',
|
||||
' $kShortcutActionPlugOutAllVirtualDisplays',
|
||||
kShortcutActionToggleShowRemoteCursor,
|
||||
kShortcutActionToggleFollowRemoteCursor,
|
||||
kShortcutActionToggleFollowRemoteWindow,
|
||||
kShortcutActionToggleZoomCursor,
|
||||
kShortcutActionToggleQualityMonitor,
|
||||
kShortcutActionToggleMute,
|
||||
kShortcutActionToggleEnableFileCopyPaste,
|
||||
kShortcutActionToggleDisableClipboard,
|
||||
kShortcutActionToggleLockAfterSessionEnd,
|
||||
kShortcutActionToggleTrueColor,
|
||||
'group:Privacy mode',
|
||||
' $kShortcutActionPrivacyMode1',
|
||||
' $kShortcutActionPrivacyMode2',
|
||||
]);
|
||||
|
||||
// Privacy mode is the last child under Display (matching the toolbar's
|
||||
// submenu order — `_DisplayMenu` adds Privacy mode after the toggles).
|
||||
final displayChildren =
|
||||
groups.firstWhere((g) => g.titleKey == 'Display').children;
|
||||
expect(displayChildren.last, isA<KeyboardShortcutActionSubgroup>());
|
||||
expect(
|
||||
(displayChildren.last as KeyboardShortcutActionSubgroup).titleKey,
|
||||
'Privacy mode',
|
||||
);
|
||||
|
||||
// Keyboard: Keyboard mode subgroup first, then direct items —
|
||||
// matching `_KeyboardMenu`.
|
||||
expect(childTokens(groups, 'Keyboard'), [
|
||||
'group:Keyboard mode',
|
||||
' $kShortcutActionKeyboardModeLegacy',
|
||||
' $kShortcutActionKeyboardModeMap',
|
||||
' $kShortcutActionKeyboardModeTranslate',
|
||||
kShortcutActionToggleInputSource,
|
||||
kShortcutActionToggleViewOnly,
|
||||
kShortcutActionToggleShowMyCursor,
|
||||
kShortcutActionToggleSwapCtrlCmd,
|
||||
kShortcutActionToggleRelativeMouseMode,
|
||||
kShortcutActionToggleReverseMouseWheel,
|
||||
kShortcutActionToggleSwapLeftRightMouse,
|
||||
]);
|
||||
});
|
||||
|
||||
test('filterKeyboardShortcutActionGroupsForPlatform drops empty groups', () {
|
||||
// Sanity: KeyboardShortcutActionGroup ctor still accepts a single direct
|
||||
// entry as a child.
|
||||
final original = [
|
||||
KeyboardShortcutActionGroup('TestGroup', [
|
||||
KeyboardShortcutActionEntry(kShortcutActionCloseTab, 'Close Tab'),
|
||||
]),
|
||||
];
|
||||
expect(original.first.children, hasLength(1));
|
||||
|
||||
// With every capability flag off, groups whose items are all behind
|
||||
// those flags get dropped. Display / Keyboard parent groups still carry
|
||||
// cross-platform direct items so they survive even when the gated
|
||||
// subgroups thin out.
|
||||
final groups = filterKeyboardShortcutActionGroupsForPlatform(
|
||||
capabilities(
|
||||
includeFullscreenShortcut: false,
|
||||
includeScreenshotShortcut: false,
|
||||
includeTabShortcuts: false,
|
||||
includeToolbarShortcut: false,
|
||||
includeCloseTabShortcut: false,
|
||||
includeSwitchSidesShortcut: false,
|
||||
includeRecordingShortcut: false,
|
||||
includeResetCanvasShortcut: false,
|
||||
includePinToolbarShortcut: false,
|
||||
includeViewModeShortcut: false,
|
||||
includeInputSourceShortcut: false,
|
||||
includeVoiceCallShortcut: false,
|
||||
),
|
||||
);
|
||||
final titles = groups.map((g) => g.titleKey).toList();
|
||||
// "Other" has nothing but platform-gated entries → dropped entirely.
|
||||
expect(titles, isNot(contains('Other')));
|
||||
// Parent groups with cross-platform direct items survive.
|
||||
expect(titles, contains('Display'));
|
||||
expect(titles, contains('Keyboard'));
|
||||
// The "View Mode" subgroup under Display is gated by includeViewModeShortcut,
|
||||
// so it must be absent from Display's surviving children.
|
||||
final displayChildren =
|
||||
groups.firstWhere((g) => g.titleKey == 'Display').children;
|
||||
final subgroupTitles = displayChildren
|
||||
.whereType<KeyboardShortcutActionSubgroup>()
|
||||
.map((s) => s.titleKey)
|
||||
.toList();
|
||||
expect(subgroupTitles, isNot(contains('View Mode')));
|
||||
// No surviving group is empty either way.
|
||||
expect(groups.every((g) => g.children.isNotEmpty), isTrue);
|
||||
// No surviving subgroup is empty.
|
||||
for (final group in groups) {
|
||||
for (final child in group.children) {
|
||||
if (child is KeyboardShortcutActionSubgroup) {
|
||||
expect(child.entries, isNotEmpty,
|
||||
reason: 'subgroup "${child.titleKey}" should not be empty');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('logicalKeyName covers the supported-keys fixture', () {
|
||||
// The fixture is the cross-language source of truth for the full set of
|
||||
// shortcut-bindable key names. Rust has a mirror test against the same
|
||||
// file (`supported_keys_match_fixture` in src/keyboard/shortcuts.rs).
|
||||
// Drift on either side breaks one of the two tests.
|
||||
final fixturePath = 'test/fixtures/supported_shortcut_keys.json';
|
||||
final fixture =
|
||||
(jsonDecode(File(fixturePath).readAsStringSync()) as List<dynamic>)
|
||||
.cast<String>()
|
||||
.toSet();
|
||||
|
||||
// Hand-rolled (LogicalKeyboardKey, name) round-trip table. Adding a key
|
||||
// requires updates in three places: the fixture, this table, and Rust's
|
||||
// matching table — that's the price of the parity guarantee.
|
||||
final mappings = <(LogicalKeyboardKey, String)>[
|
||||
for (var c = 0; c < 26; c++)
|
||||
(
|
||||
LogicalKeyboardKey(0x00000000061 + c),
|
||||
String.fromCharCode(0x61 + c),
|
||||
),
|
||||
for (var d = 0; d < 10; d++)
|
||||
(LogicalKeyboardKey(0x00000000030 + d), 'digit$d'),
|
||||
(LogicalKeyboardKey.f1, 'f1'),
|
||||
(LogicalKeyboardKey.f2, 'f2'),
|
||||
(LogicalKeyboardKey.f3, 'f3'),
|
||||
(LogicalKeyboardKey.f4, 'f4'),
|
||||
(LogicalKeyboardKey.f5, 'f5'),
|
||||
(LogicalKeyboardKey.f6, 'f6'),
|
||||
(LogicalKeyboardKey.f7, 'f7'),
|
||||
(LogicalKeyboardKey.f8, 'f8'),
|
||||
(LogicalKeyboardKey.f9, 'f9'),
|
||||
(LogicalKeyboardKey.f10, 'f10'),
|
||||
(LogicalKeyboardKey.f11, 'f11'),
|
||||
(LogicalKeyboardKey.f12, 'f12'),
|
||||
(LogicalKeyboardKey.delete, 'delete'),
|
||||
(LogicalKeyboardKey.backspace, 'backspace'),
|
||||
(LogicalKeyboardKey.tab, 'tab'),
|
||||
(LogicalKeyboardKey.space, 'space'),
|
||||
(LogicalKeyboardKey.enter, 'enter'),
|
||||
(LogicalKeyboardKey.numpadEnter, 'enter'),
|
||||
(LogicalKeyboardKey.arrowLeft, 'arrow_left'),
|
||||
(LogicalKeyboardKey.arrowRight, 'arrow_right'),
|
||||
(LogicalKeyboardKey.arrowUp, 'arrow_up'),
|
||||
(LogicalKeyboardKey.arrowDown, 'arrow_down'),
|
||||
(LogicalKeyboardKey.home, 'home'),
|
||||
(LogicalKeyboardKey.end, 'end'),
|
||||
(LogicalKeyboardKey.pageUp, 'page_up'),
|
||||
(LogicalKeyboardKey.pageDown, 'page_down'),
|
||||
(LogicalKeyboardKey.insert, 'insert'),
|
||||
];
|
||||
|
||||
// Round-trip: every (key, name) pair must agree with logicalKeyName.
|
||||
for (final (key, name) in mappings) {
|
||||
expect(logicalKeyName(key), equals(name),
|
||||
reason: 'logicalKeyName($key) should be "$name"');
|
||||
}
|
||||
|
||||
// The set of names produced by the table must equal the fixture.
|
||||
final namesFromTable = mappings.map((e) => e.$2).toSet();
|
||||
expect(namesFromTable, equals(fixture),
|
||||
reason: 'logicalKeyName vocabulary drifted from $fixturePath — update '
|
||||
'shortcut_utils.dart::logicalKeyName, the fixture, and Rust '
|
||||
'event_to_key_name together');
|
||||
|
||||
// Modifier-only / unsupported keys must return null.
|
||||
expect(logicalKeyName(LogicalKeyboardKey.shift), isNull);
|
||||
expect(logicalKeyName(LogicalKeyboardKey.escape), isNull);
|
||||
expect(logicalKeyName(LogicalKeyboardKey.f13), isNull);
|
||||
});
|
||||
|
||||
test('configurable shortcut list does not include known-removed action IDs',
|
||||
() {
|
||||
// These IDs were briefly defined without handlers (a "ghost action"
|
||||
// footgun). If you intend to re-add one of these as a real action,
|
||||
// wire up its handler and add a constant + group entry — do not just
|
||||
// resurrect the literal string below.
|
||||
//
|
||||
// Note: `toggle_privacy_mode` was once on this list but is now a real
|
||||
// implemented action (registered in shortcut_model.dart). The other
|
||||
// legacy IDs (toggle_audio, view_mode_shrink/stretch, view_mode_1_to_1)
|
||||
// were renamed: their replacements are kShortcutActionToggleMute and
|
||||
// kShortcutActionViewModeOriginal/Adaptive/Custom.
|
||||
const knownRemoved = [
|
||||
'toggle_audio',
|
||||
'view_mode_1_to_1',
|
||||
'view_mode_shrink',
|
||||
'view_mode_stretch',
|
||||
];
|
||||
final actions = idSet(kKeyboardShortcutActionGroups);
|
||||
for (final id in knownRemoved) {
|
||||
expect(actions, isNot(contains(id)),
|
||||
reason:
|
||||
'"$id" was a known ghost action — wire a real handler before re-adding it');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -624,6 +624,7 @@ void CliprdrStream_Delete(CliprdrStream *instance)
|
||||
if (instance)
|
||||
{
|
||||
free(instance->iStream.lpVtbl);
|
||||
instance->iStream.lpVtbl = NULL;
|
||||
free(instance);
|
||||
}
|
||||
}
|
||||
@@ -2160,7 +2161,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
||||
return FALSE;
|
||||
|
||||
/* add to name array */
|
||||
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2);
|
||||
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR));
|
||||
|
||||
if (!clipboard->file_names[clipboard->nFiles])
|
||||
return FALSE;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Submodule libs/hbb_common updated: f08ce5d6d0...87b11a7959
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -29,4 +29,4 @@ TODO
|
||||
|
||||
## X11
|
||||
|
||||
## OSX
|
||||
## macOS
|
||||
|
||||
82
res/audits.py
Normal file → Executable file
82
res/audits.py
Normal file → Executable 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,
|
||||
|
||||
@@ -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'"') {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
221
src/common.rs
221
src/common.rs
@@ -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")
|
||||
}
|
||||
|
||||
@@ -1187,6 +1188,10 @@ fn get_tcp_proxy_addr() -> String {
|
||||
/// 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,
|
||||
@@ -1212,52 +1217,65 @@ async fn tcp_proxy_request(
|
||||
tcp_addr
|
||||
);
|
||||
|
||||
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 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 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?;
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_http_proxy_request(req);
|
||||
conn.send(&msg_out).await?;
|
||||
|
||||
match timeout(READ_TIMEOUT, 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"),
|
||||
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"),
|
||||
}
|
||||
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![HeaderEntry {
|
||||
name: "Content-Type".into(),
|
||||
value: "application/json".into(),
|
||||
..Default::default()
|
||||
}];
|
||||
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") {
|
||||
entries.push(HeaderEntry {
|
||||
name: tmp[0].into(),
|
||||
value: tmp[1].into(),
|
||||
..Default::default()
|
||||
});
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1308,21 +1326,16 @@ fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn tcp_proxy_fallback_log_condition() -> &'static str {
|
||||
"failed or 5xx"
|
||||
}
|
||||
|
||||
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
|
||||
async fn post_request_http(url: String, body: String, header: &str) -> ResultType<(u16, String)> {
|
||||
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,
|
||||
@@ -1334,6 +1347,49 @@ async fn post_request_http(url: String, body: String, header: &str) -> ResultTyp
|
||||
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,
|
||||
@@ -1341,40 +1397,13 @@ async fn post_request_http(url: String, body: String, header: &str) -> ResultTyp
|
||||
/// - 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> {
|
||||
if should_use_raw_tcp_for_api(&url) {
|
||||
return post_request_via_tcp_proxy(&url, &body, header).await;
|
||||
}
|
||||
|
||||
let http_result = post_request_http(url.clone(), body.clone(), header).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 POST to {} {} (result: {:?}), trying TCP proxy fallback",
|
||||
tcp_proxy_log_target(&url),
|
||||
tcp_proxy_fallback_log_condition(),
|
||||
http_result
|
||||
.as_ref()
|
||||
.map(|(s, _)| *s)
|
||||
.map_err(|e| e.to_string()),
|
||||
);
|
||||
match post_request_via_tcp_proxy(&url, &body, header).await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(tcp_err) => {
|
||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||
// Fall through to return original HTTP result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return original HTTP result
|
||||
match http_result {
|
||||
Ok((_status, text)) => Ok(text),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
"POST",
|
||||
post_request_http(&url, &body, header),
|
||||
post_request_via_tcp_proxy(&url, &body, header),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -1623,32 +1652,13 @@ pub async fn http_request_sync(
|
||||
body: Option<String>,
|
||||
header: String,
|
||||
) -> ResultType<String> {
|
||||
if should_use_raw_tcp_for_api(&url) {
|
||||
return http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await;
|
||||
}
|
||||
|
||||
let http_result = http_request_http(&url, &method, body.clone(), &header).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 {} {}, trying TCP proxy fallback",
|
||||
method,
|
||||
tcp_proxy_log_target(&url),
|
||||
tcp_proxy_fallback_log_condition()
|
||||
);
|
||||
match http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(tcp_err) => {
|
||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http_result.map(|(_status, json_str)| json_str)
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
&method,
|
||||
http_request_http(&url, &method, body.clone(), &header),
|
||||
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
||||
@@ -2757,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"));
|
||||
|
||||
@@ -2874,7 +2886,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_ignores_custom_content_type() {
|
||||
fn test_parse_simple_header_respects_custom_content_type() {
|
||||
let headers = parse_simple_header("Content-Type: text/plain");
|
||||
|
||||
assert_eq!(
|
||||
@@ -2889,7 +2901,7 @@ mod tests {
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("application/json")
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2917,11 +2929,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_fallback_log_condition() {
|
||||
assert_eq!(tcp_proxy_fallback_log_condition(), "failed or 5xx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_redacts_query_only() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
265
src/keyboard.rs
265
src/keyboard.rs
@@ -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,196 @@ 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.
|
||||
//
|
||||
// NOTE: Shortcut matching intentionally happens BEFORE any key swapping
|
||||
// (swap_modifier_key) so that shortcuts bind to the physical keys pressed,
|
||||
// not the swapped keys. This makes shortcut setup intuitive: users bind
|
||||
// shortcuts to the actual keys they press, regardless of swap settings.
|
||||
// Key swapping only affects what gets sent to the remote.
|
||||
//
|
||||
// Gated on `feature = "flutter"` because the dispatch target
|
||||
// (`flutter::push_session_event`) is Flutter-only. Sciter builds never
|
||||
// call `reload_from_config`, so the cache stays disabled and the
|
||||
// matcher would no-op anyway — but we still skip the call entirely so
|
||||
// a hand-edited config can't silently swallow keys on a UI that has
|
||||
// no way to surface the action.
|
||||
//
|
||||
// `None` for session_id makes the helper resolve through
|
||||
// `flutter::get_cur_session_id()` — the rdev grab loop is process-wide
|
||||
// and has no per-event session context to thread.
|
||||
#[cfg(feature = "flutter")]
|
||||
if crate::keyboard::shortcuts::try_dispatch(None, event, keyboard_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
|
||||
if is_long_press(&event) {
|
||||
return;
|
||||
@@ -144,7 +363,20 @@ pub mod client {
|
||||
event: &Event,
|
||||
lock_modes: Option<i32>,
|
||||
session: &Session<T>,
|
||||
session_id: SessionID,
|
||||
) {
|
||||
// Shortcut intercept — see the long comment in `process_event` above
|
||||
// for the KeyPress-only / feature-gate rationale. The only difference
|
||||
// here is that the Flutter FFI path threads an explicit SessionID
|
||||
// through, so dispatch targets the exact tab the keystroke originated
|
||||
// from — no dependency on the global focus tracker.
|
||||
#[cfg(feature = "flutter")]
|
||||
if crate::keyboard::shortcuts::try_dispatch(Some(&session_id), event, keyboard_mode) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let _ = session_id;
|
||||
|
||||
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
|
||||
if is_long_press(&event) {
|
||||
return;
|
||||
@@ -341,7 +573,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 +771,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 +791,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 +987,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 +999,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 {
|
||||
|
||||
728
src/keyboard/shortcuts.rs
Normal file
728
src/keyboard/shortcuts.rs
Normal file
@@ -0,0 +1,728 @@
|
||||
//! Keyboard shortcuts for triggering session actions locally.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use hbb_common::log;
|
||||
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 SWITCH_DISPLAY_ALL: &str = "switch_display_all";
|
||||
pub const SCREENSHOT: &str = "screenshot";
|
||||
pub const INSERT_LOCK: &str = "insert_lock";
|
||||
pub const REFRESH: &str = "refresh";
|
||||
pub const TOGGLE_BLOCK_INPUT: &str = "toggle_block_input";
|
||||
pub const TOGGLE_RECORDING: &str = "toggle_recording";
|
||||
pub const SWITCH_SIDES: &str = "switch_sides";
|
||||
pub const CLOSE_TAB: &str = "close_tab";
|
||||
pub const TOGGLE_TOOLBAR: &str = "toggle_toolbar";
|
||||
pub const RESTART_REMOTE: &str = "restart_remote";
|
||||
pub const RESET_CANVAS: &str = "reset_canvas";
|
||||
pub const TOGGLE_MUTE: &str = "toggle_mute";
|
||||
pub const PIN_TOOLBAR: &str = "pin_toolbar";
|
||||
pub const VIEW_MODE_ORIGINAL: &str = "view_mode_original";
|
||||
pub const VIEW_MODE_ADAPTIVE: &str = "view_mode_adaptive";
|
||||
pub const TOGGLE_CHAT: &str = "toggle_chat";
|
||||
pub const TOGGLE_QUALITY_MONITOR: &str = "toggle_quality_monitor";
|
||||
pub const TOGGLE_SHOW_REMOTE_CURSOR: &str = "toggle_show_remote_cursor";
|
||||
pub const TOGGLE_SHOW_MY_CURSOR: &str = "toggle_show_my_cursor";
|
||||
pub const TOGGLE_DISABLE_CLIPBOARD: &str = "toggle_disable_clipboard";
|
||||
pub const PRIVACY_MODE_1: &str = "privacy_mode_1";
|
||||
pub const PRIVACY_MODE_2: &str = "privacy_mode_2";
|
||||
pub const KEYBOARD_MODE_MAP: &str = "keyboard_mode_map";
|
||||
pub const KEYBOARD_MODE_TRANSLATE: &str = "keyboard_mode_translate";
|
||||
pub const KEYBOARD_MODE_LEGACY: &str = "keyboard_mode_legacy";
|
||||
pub const CODEC_AUTO: &str = "codec_auto";
|
||||
pub const CODEC_VP8: &str = "codec_vp8";
|
||||
pub const CODEC_VP9: &str = "codec_vp9";
|
||||
pub const CODEC_AV1: &str = "codec_av1";
|
||||
pub const CODEC_H264: &str = "codec_h264";
|
||||
pub const CODEC_H265: &str = "codec_h265";
|
||||
pub const PLUG_OUT_ALL_VIRTUAL_DISPLAYS: &str = "plug_out_all_virtual_displays";
|
||||
pub const TOGGLE_RELATIVE_MOUSE_MODE: &str = "toggle_relative_mouse_mode";
|
||||
pub const TOGGLE_FOLLOW_REMOTE_CURSOR: &str = "toggle_follow_remote_cursor";
|
||||
pub const TOGGLE_FOLLOW_REMOTE_WINDOW: &str = "toggle_follow_remote_window";
|
||||
pub const TOGGLE_ZOOM_CURSOR: &str = "toggle_zoom_cursor";
|
||||
pub const TOGGLE_REVERSE_MOUSE_WHEEL: &str = "toggle_reverse_mouse_wheel";
|
||||
pub const TOGGLE_SWAP_LEFT_RIGHT_MOUSE: &str = "toggle_swap_left_right_mouse";
|
||||
pub const TOGGLE_LOCK_AFTER_SESSION_END: &str = "toggle_lock_after_session_end";
|
||||
pub const TOGGLE_TRUE_COLOR: &str = "toggle_true_color";
|
||||
pub const TOGGLE_SWAP_CTRL_CMD: &str = "toggle_swap_ctrl_cmd";
|
||||
pub const TOGGLE_ENABLE_FILE_COPY_PASTE: &str = "toggle_enable_file_copy_paste";
|
||||
pub const VIEW_MODE_CUSTOM: &str = "view_mode_custom";
|
||||
pub const IMAGE_QUALITY_BEST: &str = "image_quality_best";
|
||||
pub const IMAGE_QUALITY_BALANCED: &str = "image_quality_balanced";
|
||||
pub const IMAGE_QUALITY_LOW: &str = "image_quality_low";
|
||||
pub const SEND_CLIPBOARD_KEYSTROKES: &str = "send_clipboard_keystrokes";
|
||||
pub const TOGGLE_INPUT_SOURCE: &str = "toggle_input_source";
|
||||
pub const SWITCH_TAB_NEXT: &str = "switch_tab_next";
|
||||
pub const SWITCH_TAB_PREV: &str = "switch_tab_prev";
|
||||
pub const TOGGLE_VOICE_CALL: &str = "toggle_voice_call";
|
||||
pub const TOGGLE_VIEW_ONLY: &str = "toggle_view_only";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Modifier {
|
||||
Primary,
|
||||
Ctrl,
|
||||
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,
|
||||
/// Persistent companion to `enabled`: when true, the matcher returns early
|
||||
/// and every keystroke flows through to the remote (i.e. all bindings are
|
||||
/// suspended). Stored alongside `enabled` and `bindings` so a single
|
||||
/// reload refreshes both flags.
|
||||
#[serde(default)]
|
||||
pub pass_through: bool,
|
||||
#[serde(default)]
|
||||
pub bindings: Vec<Binding>,
|
||||
}
|
||||
|
||||
pub fn default_bindings() -> Vec<Binding> {
|
||||
let prefix = || vec![Modifier::Primary, Modifier::Alt, Modifier::Shift];
|
||||
// Defaults align with AnyDesk's M/S/I/C/Delete/Arrow/Digit conventions
|
||||
// where applicable; "P" for screenshot also matches AnyDesk.
|
||||
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() },
|
||||
Binding { action: action_id::TOGGLE_SHOW_REMOTE_CURSOR.into(), mods: prefix(), key: "m".into() },
|
||||
Binding { action: action_id::TOGGLE_MUTE.into(), mods: prefix(), key: "s".into() },
|
||||
Binding { action: action_id::TOGGLE_BLOCK_INPUT.into(), mods: prefix(), key: "i".into() },
|
||||
Binding { action: action_id::TOGGLE_CHAT.into(), mods: prefix(), key: "c".into() },
|
||||
]
|
||||
}
|
||||
|
||||
/// Match a normalized (key, modifiers) pair against the given bindings.
|
||||
/// Returns the matched action ID, or None when the matcher is off
|
||||
/// (`enabled == false`), suspended (`pass_through == true`), or no binding
|
||||
/// fires for this combo.
|
||||
///
|
||||
/// Defense-in-depth: bindings with an empty modifier list are skipped here
|
||||
/// even though the recording dialog refuses to save them. A hand-edited
|
||||
/// config (or a future writer-side bug) that lets an empty-mods binding
|
||||
/// through would otherwise turn that key's every press into a swallowed
|
||||
/// shortcut, breaking normal typing in the remote session — a much worse
|
||||
/// failure than the binding simply not firing.
|
||||
pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
|
||||
if !b.enabled || b.pass_through {
|
||||
return None;
|
||||
}
|
||||
for binding in &b.bindings {
|
||||
if binding.mods.is_empty() {
|
||||
continue;
|
||||
}
|
||||
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> {
|
||||
// iOS shares Apple's keyboard semantics with macOS — recording dialog
|
||||
// already treats iOS as `_isMac`, so the matcher must too.
|
||||
//
|
||||
// AltGr conflation: `get_modifiers_state` ORs Alt and AltGr, so an
|
||||
// AltGr+key press satisfies `Modifier::Alt`. Theoretical collision only;
|
||||
// fix at `get_modifiers_state` if a real bug surfaces.
|
||||
let mut v = Vec::new();
|
||||
if cfg!(any(target_os = "macos", target_os = "ios")) {
|
||||
if command { v.push(Modifier::Primary); }
|
||||
if ctrl { v.push(Modifier::Ctrl); }
|
||||
} else {
|
||||
if ctrl { 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::Backspace => "backspace".into(),
|
||||
Key::Tab => "tab".into(),
|
||||
Key::Space => "space".into(),
|
||||
Key::Home => "home".into(),
|
||||
Key::End => "end".into(),
|
||||
Key::PageUp => "page_up".into(),
|
||||
Key::PageDown => "page_down".into(),
|
||||
Key::Insert => "insert".into(),
|
||||
// Numpad Enter (`KpReturn`) shares the "enter" name with the main
|
||||
// Return key — matches the Web matcher (`NumpadEnter` -> "enter") and
|
||||
// matches user expectation that the two physical Enters are
|
||||
// interchangeable for shortcuts.
|
||||
Key::Return | Key::KpReturn => "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::Num0 => "digit0".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(),
|
||||
Key::F1 => "f1".into(),
|
||||
Key::F2 => "f2".into(),
|
||||
Key::F3 => "f3".into(),
|
||||
Key::F4 => "f4".into(),
|
||||
Key::F5 => "f5".into(),
|
||||
Key::F6 => "f6".into(),
|
||||
Key::F7 => "f7".into(),
|
||||
Key::F8 => "f8".into(),
|
||||
Key::F9 => "f9".into(),
|
||||
Key::F10 => "f10".into(),
|
||||
Key::F11 => "f11".into(),
|
||||
Key::F12 => "f12".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 {
|
||||
match serde_json::from_str(&raw) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to parse keyboard shortcut config: {}", e);
|
||||
Bindings::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
match CACHE.write() {
|
||||
Ok(mut w) => {
|
||||
*w = Arc::new(parsed);
|
||||
}
|
||||
Err(poison) => {
|
||||
log::error!("Keyboard shortcut cache write lock is poisoned");
|
||||
*poison.into_inner() = Arc::new(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
|
||||
/// safe to call on every keystroke.
|
||||
pub fn current() -> Arc<Bindings> {
|
||||
match CACHE.read() {
|
||||
Ok(b) => Arc::clone(&b),
|
||||
Err(poison) => {
|
||||
log::error!("Keyboard shortcut cache read lock is poisoned");
|
||||
Arc::clone(&poison.into_inner())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ── Two known minor warts. DO NOT add global state to "fix" either: ──
|
||||
///
|
||||
/// 1. Orphan KeyRelease forwarded to peer.
|
||||
/// When a shortcut matches we eat the KeyPress, but the matching
|
||||
/// KeyRelease (whose `event_type` returns None from `event_to_key_name`)
|
||||
/// still flows through to the peer. The remote sees a release for a
|
||||
/// press it never received. Every input server we forward to ignores
|
||||
/// releases for unpressed keys, so user-visible impact is nil — the
|
||||
/// pre-existing hard-coded screenshot-shortcut path had the same shape
|
||||
/// for years without a single bug report.
|
||||
///
|
||||
/// 2. OS auto-repeat re-dispatches a held shortcut.
|
||||
/// rdev does not expose an `is_repeat` flag, so a held combo
|
||||
/// (Cmd+Alt+Shift+P) would dispatch every ~30-50ms while the keys are
|
||||
/// down — toggle actions oscillate, screenshot fires many times. In
|
||||
/// practice the OS initial auto-repeat delay is ~250ms and a normal
|
||||
/// shortcut press is 50-100ms, so the user has to *deliberately* hold
|
||||
/// the combo to hit this. The Web side gets a free fix via the
|
||||
/// browser's `KeyboardEvent.repeat`; on native we accept the wart.
|
||||
///
|
||||
/// The "fix" for either would be a process-global `HashSet<rdev::Key>` (or
|
||||
/// equivalent) with paired insert-on-press / remove-on-release logic in
|
||||
/// both `process_event*` paths plus a clear-on-leave hook. The cost:
|
||||
///
|
||||
/// * Lock contention on the hot keystroke path.
|
||||
/// * Three input sources (rdev grab, Flutter raw key, Flutter USB HID)
|
||||
/// all converge to `rdev::Key`, so correctness depends on
|
||||
/// `rdev::key_from_code` / `rdev::usb_hid_key_from_code` /
|
||||
/// `rdev::get_win_key` agreeing on the same physical key — the project
|
||||
/// already has scattered swap_modifier_key / ControlLeft↔MetaLeft
|
||||
/// fixups for places where they historically *didn't* agree. Any new
|
||||
/// mismatch silently leaks the set; "shortcut stopped responding"
|
||||
/// after a stuck entry is a worse failure mode than "shortcut fired
|
||||
/// twice."
|
||||
/// * Leak risk on focus loss / disconnect, requiring a clear hook the
|
||||
/// callers must remember to invoke.
|
||||
/// * Two new code paths to keep in lockstep with two existing keyboard
|
||||
/// pipelines.
|
||||
///
|
||||
/// For two warts whose user-visible impact is nil-to-marginal, that
|
||||
/// trade-off goes the wrong way. Leave it. If a real user bug shows up
|
||||
/// here, revisit then with concrete repro — not pre-emptively.
|
||||
pub fn match_event(event: &rdev::Event) -> Option<String> {
|
||||
let bindings = current();
|
||||
if !bindings.enabled || bindings.pass_through {
|
||||
return None;
|
||||
}
|
||||
// Note: `match_normalized` re-checks both flags below — this short-circuit
|
||||
// is just to avoid the `event_to_key_name` + `get_modifiers_state` work
|
||||
// in the common bypass case.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Match `event` against the cached bindings; if it matched, push a
|
||||
/// `shortcut_triggered` Flutter session event and return `true` so the caller
|
||||
/// can `return` early. Returns `false` when no shortcut fired (caller should
|
||||
/// continue with normal key handling).
|
||||
///
|
||||
/// `session_id`:
|
||||
/// * `Some(&id)` — Flutter FFI path: dispatch to the exact session whose key
|
||||
/// event we're processing. No dependence on the global focus tracker.
|
||||
/// * `None` — rdev grab loop: the loop is process-wide and has no way to know
|
||||
/// which Flutter session id the keystroke was meant for, so route to the
|
||||
/// globally-current session via `flutter::get_cur_session_id()`.
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn try_dispatch(
|
||||
session_id: Option<&hbb_common::SessionID>,
|
||||
event: &rdev::Event,
|
||||
keyboard_mode: &str,
|
||||
) -> bool {
|
||||
let Some(action_id) = match_event(event) else {
|
||||
return false;
|
||||
};
|
||||
let resolved;
|
||||
let sid = match session_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
resolved = crate::flutter::get_cur_session_id();
|
||||
&resolved
|
||||
}
|
||||
};
|
||||
crate::keyboard::release_remote_keys(keyboard_mode);
|
||||
crate::flutter::push_session_event(sid, "shortcut_triggered", vec![("action", &action_id)]);
|
||||
true
|
||||
}
|
||||
|
||||
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,
|
||||
// macOS users can bind shortcuts that use Control independently
|
||||
// of Command. On Win/Linux this variant should never appear in a
|
||||
// saved binding (`normalize_modifiers` collapses Ctrl into
|
||||
// Primary), but we still give it a distinct bit so a hand-edited
|
||||
// config can't accidentally collide with another modifier.
|
||||
Modifier::Ctrl => 8,
|
||||
};
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
fn mods_equal(a: &[Modifier], b: &[Modifier]) -> bool {
|
||||
mods_bits(a) == mods_bits(b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_press(k: rdev::Key) -> rdev::Event {
|
||||
rdev::Event {
|
||||
time: std::time::SystemTime::now(),
|
||||
unicode: None,
|
||||
platform_code: 0,
|
||||
position_code: 0,
|
||||
event_type: rdev::EventType::KeyPress(k),
|
||||
usb_hid: 0,
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
extra_data: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_to_key_name_handles_f_keys() {
|
||||
use rdev::Key;
|
||||
assert_eq!(event_to_key_name(&make_press(Key::F1)), Some("f1".into()));
|
||||
assert_eq!(event_to_key_name(&make_press(Key::F5)), Some("f5".into()));
|
||||
assert_eq!(event_to_key_name(&make_press(Key::F12)), Some("f12".into()));
|
||||
}
|
||||
|
||||
/// Cross-language parity for default bindings. The fixture file is the
|
||||
/// shared source of truth — Dart has a mirror test against the same file
|
||||
/// (`kDefaultShortcutBindings matches fixture` in
|
||||
/// `flutter/test/keyboard_shortcuts_test.dart`). Any drift on either
|
||||
/// side breaks one of the two tests.
|
||||
#[test]
|
||||
fn default_bindings_match_fixture_json() {
|
||||
let fixture: serde_json::Value = serde_json::from_str(include_str!(
|
||||
"../../flutter/test/fixtures/default_keyboard_shortcuts.json"
|
||||
))
|
||||
.expect("fixture is valid JSON");
|
||||
let actual: serde_json::Value =
|
||||
serde_json::to_value(default_bindings()).expect("serialize defaults");
|
||||
assert_eq!(
|
||||
fixture, actual,
|
||||
"default_bindings() drifted from \
|
||||
flutter/test/fixtures/default_keyboard_shortcuts.json — update \
|
||||
shortcuts.rs, the fixture, and Dart kDefaultShortcutBindings together"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_to_key_name_treats_numpad_enter_as_enter() {
|
||||
use rdev::{Event, EventType, Key};
|
||||
let make = |k: Key| Event {
|
||||
time: std::time::SystemTime::now(),
|
||||
unicode: None,
|
||||
platform_code: 0,
|
||||
position_code: 0,
|
||||
event_type: EventType::KeyPress(k),
|
||||
usb_hid: 0,
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
extra_data: 0,
|
||||
};
|
||||
assert_eq!(event_to_key_name(&make(Key::Return)), Some("enter".into()));
|
||||
assert_eq!(event_to_key_name(&make(Key::KpReturn)), Some("enter".into()));
|
||||
}
|
||||
|
||||
#[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(&action_id::TOGGLE_SHOW_REMOTE_CURSOR));
|
||||
assert!(actions.contains(&action_id::TOGGLE_MUTE));
|
||||
assert!(actions.contains(&action_id::TOGGLE_BLOCK_INPUT));
|
||||
assert!(actions.contains(&action_id::TOGGLE_CHAT));
|
||||
// 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_pass_through() {
|
||||
let bindings = Bindings {
|
||||
enabled: true,
|
||||
pass_through: true,
|
||||
bindings: default_bindings(),
|
||||
};
|
||||
let result = match_normalized(
|
||||
"p",
|
||||
&[Modifier::Primary, Modifier::Alt, Modifier::Shift],
|
||||
&bindings,
|
||||
);
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_returns_none_when_disabled() {
|
||||
let bindings = Bindings { enabled: false, pass_through: 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, pass_through: false, 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, pass_through: false, 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, pass_through: false, 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,
|
||||
pass_through: false,
|
||||
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!(any(target_os = "macos", target_os = "ios")) {
|
||||
// On Apple platforms Ctrl is NOT primary
|
||||
assert!(!mods.contains(&Modifier::Primary));
|
||||
assert!(mods.contains(&Modifier::Ctrl));
|
||||
} else {
|
||||
assert!(mods.contains(&Modifier::Primary));
|
||||
}
|
||||
assert!(mods.contains(&Modifier::Alt));
|
||||
assert!(mods.contains(&Modifier::Shift));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_normalization_command_is_primary_on_apple() {
|
||||
let mods = normalize_modifiers(true, false, true, /*command=*/true);
|
||||
if cfg!(any(target_os = "macos", target_os = "ios")) {
|
||||
assert!(mods.contains(&Modifier::Primary));
|
||||
} else {
|
||||
// On Win/Linux Command/Meta is NOT primary
|
||||
assert!(!mods.contains(&Modifier::Primary));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_refuses_zero_modifier_bindings() {
|
||||
// Defense-in-depth: a hand-edited config with empty `mods` must NOT
|
||||
// turn every plain "P" press into a screenshot shortcut, which would
|
||||
// swallow all typing in the remote session. The recording dialog
|
||||
// already refuses to save such bindings, but the matcher must hold
|
||||
// the line independently.
|
||||
let bindings = Bindings {
|
||||
enabled: true,
|
||||
pass_through: false,
|
||||
bindings: vec![Binding {
|
||||
action: "screenshot".into(),
|
||||
mods: vec![],
|
||||
key: "p".into(),
|
||||
}],
|
||||
};
|
||||
assert_eq!(match_normalized("p", &[], &bindings), None);
|
||||
// Even with extra modifiers held by the user, a zero-mod binding
|
||||
// still doesn't match (no shape of held modifiers can equal the
|
||||
// empty saved set after the empty-check skips the entry).
|
||||
assert_eq!(
|
||||
match_normalized("p", &[Modifier::Primary], &bindings),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Cross-language parity for the full set of shortcut-bindable key
|
||||
/// names (not just the defaults). The fixture lists every name the
|
||||
/// matcher accepts; this test verifies the (rdev::Key → name) round-trip
|
||||
/// covers exactly that set. Dart has a mirror test against the same
|
||||
/// fixture (`logicalKeyName covers the supported-keys fixture` in
|
||||
/// `flutter/test/keyboard_shortcuts_test.dart`).
|
||||
///
|
||||
/// Adding a key requires updates in three places: the fixture, this
|
||||
/// table, and the Dart `logicalKeyName` — that's the price of the
|
||||
/// parity guarantee. Drift on any side breaks one of the two tests.
|
||||
#[test]
|
||||
fn supported_keys_match_fixture() {
|
||||
use rdev::Key;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let table: &[(&str, Key)] = &[
|
||||
("a", Key::KeyA), ("b", Key::KeyB), ("c", Key::KeyC),
|
||||
("d", Key::KeyD), ("e", Key::KeyE), ("f", Key::KeyF),
|
||||
("g", Key::KeyG), ("h", Key::KeyH), ("i", Key::KeyI),
|
||||
("j", Key::KeyJ), ("k", Key::KeyK), ("l", Key::KeyL),
|
||||
("m", Key::KeyM), ("n", Key::KeyN), ("o", Key::KeyO),
|
||||
("p", Key::KeyP), ("q", Key::KeyQ), ("r", Key::KeyR),
|
||||
("s", Key::KeyS), ("t", Key::KeyT), ("u", Key::KeyU),
|
||||
("v", Key::KeyV), ("w", Key::KeyW), ("x", Key::KeyX),
|
||||
("y", Key::KeyY), ("z", Key::KeyZ),
|
||||
("digit0", Key::Num0), ("digit1", Key::Num1),
|
||||
("digit2", Key::Num2), ("digit3", Key::Num3),
|
||||
("digit4", Key::Num4), ("digit5", Key::Num5),
|
||||
("digit6", Key::Num6), ("digit7", Key::Num7),
|
||||
("digit8", Key::Num8), ("digit9", Key::Num9),
|
||||
("f1", Key::F1), ("f2", Key::F2), ("f3", Key::F3),
|
||||
("f4", Key::F4), ("f5", Key::F5), ("f6", Key::F6),
|
||||
("f7", Key::F7), ("f8", Key::F8), ("f9", Key::F9),
|
||||
("f10", Key::F10), ("f11", Key::F11), ("f12", Key::F12),
|
||||
("delete", Key::Delete),
|
||||
("backspace", Key::Backspace),
|
||||
("tab", Key::Tab),
|
||||
("space", Key::Space),
|
||||
("enter", Key::Return),
|
||||
("enter", Key::KpReturn),
|
||||
("arrow_left", Key::LeftArrow),
|
||||
("arrow_right", Key::RightArrow),
|
||||
("arrow_up", Key::UpArrow),
|
||||
("arrow_down", Key::DownArrow),
|
||||
("home", Key::Home),
|
||||
("end", Key::End),
|
||||
("page_up", Key::PageUp),
|
||||
("page_down", Key::PageDown),
|
||||
("insert", Key::Insert),
|
||||
];
|
||||
|
||||
// Round-trip: every entry in the table must map through
|
||||
// event_to_key_name to its declared name.
|
||||
for (name, key) in table {
|
||||
assert_eq!(
|
||||
event_to_key_name(&make_press(*key)).as_deref(),
|
||||
Some(*name),
|
||||
"rdev::Key::{:?} should map to {:?}",
|
||||
key, name,
|
||||
);
|
||||
}
|
||||
|
||||
// The set of names produced by the table must equal the fixture.
|
||||
let actual: BTreeSet<&str> = table.iter().map(|(n, _)| *n).collect();
|
||||
let fixture_raw: Vec<String> = serde_json::from_str(include_str!(
|
||||
"../../flutter/test/fixtures/supported_shortcut_keys.json"
|
||||
))
|
||||
.expect("fixture is valid JSON");
|
||||
let expected: BTreeSet<&str> =
|
||||
fixture_raw.iter().map(String::as_str).collect();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"event_to_key_name vocabulary drifted from \
|
||||
flutter/test/fixtures/supported_shortcut_keys.json — update \
|
||||
shortcuts.rs, the fixture, and Dart logicalKeyName together"
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -729,19 +729,53 @@ 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", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
776
src/lang/be.rs
776
src/lang/be.rs
File diff suppressed because it is too large
Load Diff
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", "为下列每项会话操作绑定一个组合键。每个绑定至少需要包含一个修饰符。"),
|
||||
("shortcut-passthrough-tip", "开启后,所有已绑定的组合键都会原样转发到远端。适合在某个组合键与远端需要使用的快捷键冲突时打开。"),
|
||||
("Pass-through to remote", "穿透到远端"),
|
||||
("Reset to defaults", "恢复默认设置"),
|
||||
("shortcut-reset-confirm-tip", "这将以默认快捷键替换所有当前绑定。是否继续?"),
|
||||
("Monitor", "显示器"),
|
||||
("Keyboard", "键盘"),
|
||||
("Toggle fullscreen", "切换全屏"),
|
||||
("Switch to next display", "切换到下一个显示器"),
|
||||
("Switch to previous display", "切换到上一个显示器"),
|
||||
("All monitors", "所有显示器"),
|
||||
("Monitor #{}", "{} 号显示器"),
|
||||
("Switch to next tab", "切换到下一个标签"),
|
||||
("Switch to previous tab", "切换到上一个标签"),
|
||||
("Toggle session recording", "切换会话录制"),
|
||||
("Close tab", "关闭标签页"),
|
||||
("Toggle toolbar", "切换工具栏可见性"),
|
||||
("Toggle input source", "切换输入源"),
|
||||
("Edit", "编辑"),
|
||||
("Save", "保存"),
|
||||
("Set Shortcut", "设置快捷键"),
|
||||
("shortcut-recording-instruction", "请按下您想使用的组合键。"),
|
||||
("shortcut-recording-press-keys-tip", "请按下组合键..."),
|
||||
("shortcut-must-include-modifiers", "必须至少包含一个修饰符:{}"),
|
||||
("shortcut-already-bound-to", "已绑定到"),
|
||||
("Replace", "替换"),
|
||||
("Valid", "有效"),
|
||||
("shortcut-mobile-physical-keyboard-tip", "录制需要使用物理键盘,不支持软键盘。"),
|
||||
("shortcut-key-not-supported", "“{}” 不能用作快捷键。"),
|
||||
("On", "开"),
|
||||
("Off", "关"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -741,7 +741,41 @@ 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."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -274,5 +274,14 @@ 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."),
|
||||
("shortcut-page-description", "Bind a key combination to each session action below. Each binding must include at least one modifier."),
|
||||
("shortcut-passthrough-tip", "When on, every bound combination is forwarded to the remote. Useful when a binding collides with something you need on the remote."),
|
||||
("shortcut-reset-confirm-tip", "This will replace all current bindings with the default set. Continue?"),
|
||||
("shortcut-recording-instruction", "Press the key combination you want to use."),
|
||||
("shortcut-recording-press-keys-tip", "Press a key combination..."),
|
||||
("shortcut-must-include-modifiers", "Must include at least one modifier: {}"),
|
||||
("shortcut-already-bound-to", "Already bound to"),
|
||||
("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."),
|
||||
("shortcut-key-not-supported", "\"{}\" can't be used as a shortcut."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -741,7 +741,41 @@ 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 d’affichage"),
|
||||
("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é."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
781
src/lang/gu.rs
Normal file
781
src/lang/gu.rs
Normal file
@@ -0,0 +1,781 @@
|
||||
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 રેન્ડરિંગ વાપરો"),
|
||||
("Use D3D rendering", ""),
|
||||
("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", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
781
src/lang/hi.rs
Normal file
781
src/lang/hi.rs
Normal file
@@ -0,0 +1,781 @@
|
||||
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 रेंडरिंग का उपयोग करें"),
|
||||
("Use D3D rendering", ""),
|
||||
("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", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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,41 @@ 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."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -741,7 +741,41 @@ 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."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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,52 @@ 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", "プリセットパスワードが現在使用されています"),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -741,7 +741,41 @@ 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", "현재 사전 설정된 비밀번호가 사용 중입니다."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
781
src/lang/ml.rs
Normal file
781
src/lang/ml.rs
Normal file
@@ -0,0 +1,781 @@
|
||||
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 റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
|
||||
("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", "അപ്ഡേറ്റ് പരാജയപ്പെട്ടു, 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", "പ്രീസെറ്റ് പാസ്വേഡ് ഉപയോഗത്തിലാണ്."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -743,5 +743,39 @@ 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", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -741,7 +741,41 @@ 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."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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,42 @@ 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."),
|
||||
("Keyboard Shortcuts", ""),
|
||||
("Configure shortcuts...", ""),
|
||||
("Enable keyboard shortcuts in remote session", ""),
|
||||
("shortcut-page-description", ""),
|
||||
("shortcut-passthrough-tip", ""),
|
||||
("Pass-through to remote", ""),
|
||||
("Reset to defaults", ""),
|
||||
("shortcut-reset-confirm-tip", ""),
|
||||
("Monitor", ""),
|
||||
("Keyboard", ""),
|
||||
("Toggle fullscreen", ""),
|
||||
("Switch to next display", ""),
|
||||
("Switch to previous display", ""),
|
||||
("All monitors", ""),
|
||||
("Monitor #{}", ""),
|
||||
("Switch to next tab", ""),
|
||||
("Switch to previous tab", ""),
|
||||
("Toggle session recording", ""),
|
||||
("Close tab", ""),
|
||||
("Toggle toolbar", ""),
|
||||
("Toggle input source", ""),
|
||||
("Edit", ""),
|
||||
("Save", ""),
|
||||
("Set Shortcut", ""),
|
||||
("shortcut-recording-instruction", ""),
|
||||
("shortcut-recording-press-keys-tip", ""),
|
||||
("shortcut-must-include-modifiers", ""),
|
||||
("shortcut-already-bound-to", ""),
|
||||
("Replace", ""),
|
||||
("Valid", ""),
|
||||
("shortcut-mobile-physical-keyboard-tip", ""),
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user