mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-07 06:38:11 +03:00
Compare commits
70 Commits
e3b6e4eaf0
...
keyboard-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f02cd9c0f6 | ||
|
|
170516572e | ||
|
|
285e29d2dc | ||
|
|
aab34b2338 | ||
|
|
ad1e5330e9 | ||
|
|
ca4647ddd6 | ||
|
|
7004acae46 | ||
|
|
899dd46f5b | ||
|
|
dba5fea66f | ||
|
|
c457b0e7d3 | ||
|
|
c0da4a6645 | ||
|
|
9d8df6a226 | ||
|
|
02da7132e7 |
15
.github/workflows/winget.yml
vendored
15
.github/workflows/winget.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.6"
|
||||
release-tag: "1.4.6"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -311,7 +311,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||
}
|
||||
val idStopService = 2
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y"
|
||||
if (!hideStopService) {
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
}
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
idShowRustDesk -> {
|
||||
@@ -389,4 +392,3 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ object FFI {
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
external fun setCodecInfo(info: String)
|
||||
external fun getLocalOption(key: String): String
|
||||
external fun getBuildinOption(key: String): String
|
||||
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||
external fun isServiceClipboardEnabled(): Boolean
|
||||
}
|
||||
|
||||
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 |
@@ -7,7 +7,7 @@
|
||||
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||
#
|
||||
|
||||
# The script is invoked by F-Droid builder system ste-by-step.
|
||||
# The script is invoked by F-Droid builder system step-by-step.
|
||||
#
|
||||
# It accepts the following arguments:
|
||||
#
|
||||
@@ -16,7 +16,6 @@
|
||||
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||
# - The build step to execute:
|
||||
#
|
||||
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||
# + prebuild: patch sources and do other stuff before the build
|
||||
# + build: perform actual build of APK file
|
||||
#
|
||||
@@ -184,13 +183,9 @@ prebuild)
|
||||
fi
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
@@ -316,6 +311,18 @@ prebuild)
|
||||
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
||||
|
||||
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
||||
# Find first libclang.so and set BRIDGE_LLVM_PATH
|
||||
|
||||
BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)"
|
||||
|
||||
if [ -z "${BRIDGE_LLVM_PATH}" ]; then
|
||||
echo 'ERROR: Can not find libclang.so for bridge generator!' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||
|
||||
# Install Flutter bridge version
|
||||
|
||||
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
||||
@@ -344,7 +351,8 @@ prebuild)
|
||||
|
||||
flutter_rust_bridge_codegen \
|
||||
--rust-input ./src/flutter_ffi.rs \
|
||||
--dart-output ./flutter/lib/generated_bridge.dart
|
||||
--dart-output ./flutter/lib/generated_bridge.dart \
|
||||
--llvm-path "${BRIDGE_LLVM_PATH}"
|
||||
|
||||
# Add bridge files to save-list
|
||||
|
||||
@@ -355,13 +363,15 @@ prebuild)
|
||||
git checkout '*'
|
||||
git clean -dffx
|
||||
git reset
|
||||
|
||||
unset BRIDGE_LLVM_PATH
|
||||
fi
|
||||
|
||||
# Install Flutter version for RustDesk library build
|
||||
|
||||
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
|
||||
# gms is not in these files now, but we still keep the following line for future reference(maybe).
|
||||
|
||||
sed \
|
||||
-i \
|
||||
@@ -414,13 +424,9 @@ build)
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
|
||||
@@ -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,11 +2387,24 @@ 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 {
|
||||
await bind.mainSetPermanentPassword(password: password);
|
||||
showToast(translate('Successful'));
|
||||
final ok =
|
||||
await bind.mainSetPermanentPasswordWithResult(password: password);
|
||||
showToast(translate(ok ? 'Successful' : 'Failed'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -175,6 +177,7 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||
const String kOptionHideStopService = "hide-stop-service";
|
||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
@@ -186,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";
|
||||
|
||||
@@ -908,12 +908,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
final p0 = TextEditingController(text: "");
|
||||
final p1 = TextEditingController(text: "");
|
||||
var errMsg0 = "";
|
||||
var errMsg1 = "";
|
||||
final RxString rxPass = pw.trim().obs;
|
||||
final localPasswordSet =
|
||||
(await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
|
||||
final permanentPasswordSet =
|
||||
(await bind.mainGetCommon(key: "permanent-password-set")) == "true";
|
||||
final presetPassword = permanentPasswordSet && !localPasswordSet;
|
||||
var canSubmit = false;
|
||||
final RxString rxPass = "".obs;
|
||||
final rules = [
|
||||
DigitValidationRule(),
|
||||
UppercaseValidationRule(),
|
||||
@@ -922,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
final statusTip = localPasswordSet
|
||||
? translate('password-hidden-tip')
|
||||
: (presetPassword ? translate('preset-password-in-use-tip') : '');
|
||||
final showStatusTipOnMobile =
|
||||
statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
updateCanSubmit() {
|
||||
canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
submit() async {
|
||||
if (!canSubmit) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
@@ -947,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
|
||||
if (!ok) {
|
||||
setState(() {
|
||||
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pass.isNotEmpty) {
|
||||
notEmptyCallback?.call();
|
||||
}
|
||||
@@ -955,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set Password")),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.key, color: MyTheme.accent),
|
||||
Text(translate("Set Password")).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 6.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -978,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
rxPass.value = value.trim();
|
||||
setState(() {
|
||||
errMsg0 = '';
|
||||
updateCanSubmit();
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
@@ -989,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
children: [
|
||||
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
|
||||
],
|
||||
).marginSymmetric(vertical: 8),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -1005,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
errMsg1 = '';
|
||||
updateCanSubmit();
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
@@ -1012,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
if (statusTip.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info, color: Colors.amber, size: 18)
|
||||
.marginOnly(right: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
statusTip,
|
||||
style: const TextStyle(fontSize: 13, height: 1.1),
|
||||
))
|
||||
],
|
||||
).marginOnly(top: 6, bottom: 2),
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||
),
|
||||
Obx(() => Wrap(
|
||||
runSpacing: 8,
|
||||
runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
|
||||
spacing: 4,
|
||||
children: rules.map((e) {
|
||||
var checked = e.validate(rxPass.value.trim());
|
||||
@@ -1036,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
actions: (() {
|
||||
final cancelButton = dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
);
|
||||
final removeButton = dialogButton(
|
||||
"Remove",
|
||||
icon: Icon(Icons.delete_outline_rounded),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final ok =
|
||||
await bind.mainSetPermanentPasswordWithResult(password: "");
|
||||
if (!ok) {
|
||||
setState(() {
|
||||
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||
});
|
||||
return;
|
||||
}
|
||||
close();
|
||||
},
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||
);
|
||||
final okButton = dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: canSubmit ? submit : null,
|
||||
);
|
||||
if (!isDesktop && !isWebDesktop && localPasswordSet) {
|
||||
return [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
cancelButton,
|
||||
const SizedBox(width: 4),
|
||||
removeButton,
|
||||
const SizedBox(width: 4),
|
||||
okButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
cancelButton,
|
||||
if (localPasswordSet) removeButton,
|
||||
okButton,
|
||||
];
|
||||
})(),
|
||||
onSubmit: canSubmit ? submit : null,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -458,18 +500,27 @@ class _GeneralState extends State<_General> {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return _Card(title: 'Service', children: [
|
||||
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||
() async {
|
||||
serviceBtnEnabled.value = false;
|
||||
await start_service(serviceStop.value);
|
||||
// enable the button after 1 second
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
serviceBtnEnabled.value = true;
|
||||
});
|
||||
}();
|
||||
}, enabled: serviceBtnEnabled.value))
|
||||
]);
|
||||
final hideStopService =
|
||||
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||
|
||||
return Obx(() {
|
||||
if (hideStopService && !serviceStop.value) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return _Card(title: 'Service', children: [
|
||||
_Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||
() async {
|
||||
serviceBtnEnabled.value = false;
|
||||
await start_service(serviceStop.value);
|
||||
// enable the button after 1 second
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
serviceBtnEnabled.value = true;
|
||||
});
|
||||
}();
|
||||
}, enabled: serviceBtnEnabled.value)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
@@ -1100,8 +1151,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
if (value ==
|
||||
passwordValues[passwordKeys
|
||||
.indexOf(kUsePermanentPassword)] &&
|
||||
(await bind.mainGetPermanentPassword())
|
||||
.isEmpty) {
|
||||
(await bind.mainGetCommon(
|
||||
key: "permanent-password-set")) !=
|
||||
"true") {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
await callback();
|
||||
return;
|
||||
@@ -2482,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()
|
||||
@@ -2525,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),
|
||||
@@ -2936,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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -150,7 +150,8 @@ class _DropDownAction extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (value == kUsePermanentPassword &&
|
||||
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||
(await bind.mainGetCommon(key: "permanent-password-set")) !=
|
||||
"true") {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
callback();
|
||||
return;
|
||||
@@ -582,10 +583,13 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
final hasAudioPermission = androidVersion >= 30;
|
||||
final hideStopService =
|
||||
isAndroid &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||
return PaddingCard(
|
||||
title: translate("Permissions"),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
serverModel.mediaOk
|
||||
serverModel.mediaOk && !hideStopService
|
||||
? ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
@@ -595,14 +599,15 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
label: Text(translate("Stop service")))
|
||||
.marginOnly(bottom: 8)
|
||||
: SizedBox.shrink(),
|
||||
PermissionRow(
|
||||
translate("Screen Capture"),
|
||||
serverModel.mediaOk,
|
||||
!serverModel.mediaOk &&
|
||||
gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||
? () => showScamWarning(context, serverModel)
|
||||
: serverModel.toggleService),
|
||||
if (!hideStopService || !serverModel.mediaOk)
|
||||
PermissionRow(
|
||||
translate("Screen Capture"),
|
||||
serverModel.mediaOk,
|
||||
!serverModel.mediaOk &&
|
||||
gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||
? () => showScamWarning(context, serverModel)
|
||||
: serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -12,100 +12,6 @@ void _showSuccess() {
|
||||
showToast(translate("Successful"));
|
||||
}
|
||||
|
||||
void _showError() {
|
||||
showToast(translate("Error"));
|
||||
}
|
||||
|
||||
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
var validateLength = false;
|
||||
var validateSame = false;
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
close();
|
||||
dialogManager.showLoading(translate("Waiting"));
|
||||
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
||||
dialogManager.dismissAll();
|
||||
_showSuccess();
|
||||
} else {
|
||||
dialogManager.dismissAll();
|
||||
_showError();
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Set your own password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Password'),
|
||||
),
|
||||
controller: p0,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = v.trim().length > 5;
|
||||
if (validateLength != val) {
|
||||
// use delay to make setState success
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateLength = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('Too short, at least 6 characters.');
|
||||
},
|
||||
).workaroundFreezeLinuxMint(),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Confirmation'),
|
||||
),
|
||||
controller: p1,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = p0.text == v;
|
||||
if (validateSame != val) {
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateSame = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('The confirmation is not identical.');
|
||||
},
|
||||
).workaroundFreezeLinuxMint(),
|
||||
])),
|
||||
onCancel: close,
|
||||
onSubmit: (validateLength && validateSame) ? submit : null,
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: (validateLength && validateSame) ? submit : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void setTemporaryPasswordLengthDialog(
|
||||
OverlayDialogManager dialogManager) async {
|
||||
List<String> lengths = ['6', '8', '10'];
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -1016,19 +1022,31 @@ class FfiModel with ChangeNotifier {
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) async {
|
||||
final showNoteEdit = parent.target != null &&
|
||||
final noteAllowed = parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
(title == "Connection Error" || type == "restarting") &&
|
||||
!hasRetry;
|
||||
(title == "Connection Error" || type == "restarting");
|
||||
final showNoteEdit = noteAllowed && !hasRetry;
|
||||
if (showNoteEdit) {
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
} else {
|
||||
VoidCallback? onSubmit;
|
||||
if (noteAllowed && hasRetry) {
|
||||
final ffi = parent.target!;
|
||||
onSubmit = () async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: ffi, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
};
|
||||
}
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
reconnectTimeout: hasRetry ? _reconnects : null,
|
||||
onSubmit: onSubmit);
|
||||
}
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
@@ -3611,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
|
||||
@@ -3640,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,
|
||||
@@ -3913,6 +3933,7 @@ class FFI {
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.callbacksOnFirstImage.clear();
|
||||
shortcutModel.clear();
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
@@ -3920,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);
|
||||
}
|
||||
|
||||
@@ -471,17 +471,6 @@ class ServerModel with ChangeNotifier {
|
||||
WakelockManager.disable(_wakelockKey);
|
||||
}
|
||||
|
||||
Future<bool> setPermanentPassword(String newPW) async {
|
||||
await bind.mainSetPermanentPassword(password: newPW);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
if (newPW == pw) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fetchID() async {
|
||||
final id = await bind.mainGetMyId();
|
||||
if (id != _serverId.id) {
|
||||
|
||||
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']);
|
||||
@@ -1159,10 +1175,6 @@ class RustdeskImpl {
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> mainGetPermanentPassword({dynamic hint}) {
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> mainGetFingerprint({dynamic hint}) {
|
||||
return Future.value('');
|
||||
}
|
||||
@@ -1180,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();
|
||||
}
|
||||
|
||||
@@ -1346,9 +1367,9 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("mainUpdateTemporaryPassword");
|
||||
}
|
||||
|
||||
Future<void> mainSetPermanentPassword(
|
||||
Future<bool> mainSetPermanentPasswordWithResult(
|
||||
{required String password, dynamic hint}) {
|
||||
throw UnimplementedError("mainSetPermanentPassword");
|
||||
throw UnimplementedError("mainSetPermanentPasswordWithResult");
|
||||
}
|
||||
|
||||
Future<bool> mainCheckSuperUserPermission({dynamic hint}) {
|
||||
@@ -1542,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: 48c37de3e6...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(),
|
||||
|
||||
@@ -119,10 +119,13 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
|
||||
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
|
||||
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version.";
|
||||
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
|
||||
"Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.";
|
||||
"wayland-requires-higher-linux-version";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str =
|
||||
"xdp-portal-unavailable";
|
||||
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
|
||||
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||
|
||||
@@ -3867,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)) => {
|
||||
|
||||
588
src/common.rs
588
src/common.rs
@@ -39,7 +39,7 @@ use hbb_common::{
|
||||
|
||||
use crate::{
|
||||
hbbs_http::{create_http_client_async, get_url_for_tls},
|
||||
ui_interface::{get_option, is_installed, set_option},
|
||||
ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -1086,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
|
||||
|
||||
#[inline]
|
||||
pub fn is_public(url: &str) -> bool {
|
||||
let url = url.to_ascii_lowercase();
|
||||
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
||||
}
|
||||
|
||||
@@ -1123,22 +1124,286 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
|
||||
format!("{}/api/audit/{}", url, typ)
|
||||
}
|
||||
|
||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||
/// Check if we should use raw TCP proxy for API calls.
|
||||
/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
|
||||
/// and the target URL belongs to the configured non-public API host.
|
||||
#[inline]
|
||||
fn should_use_raw_tcp_for_api(url: &str) -> bool {
|
||||
get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
|
||||
&& !use_ws()
|
||||
&& is_tcp_proxy_api_target(url)
|
||||
}
|
||||
|
||||
/// Check if we can attempt raw TCP proxy fallback for this target URL.
|
||||
#[inline]
|
||||
fn can_fallback_to_raw_tcp(url: &str) -> bool {
|
||||
!use_ws() && is_tcp_proxy_api_target(url)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
|
||||
if api_url.is_empty() || is_public(api_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target_host = url::Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||
let api_host = url::Url::parse(api_url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||
|
||||
matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_tcp_proxy_api_target(url: &str) -> bool {
|
||||
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
|
||||
}
|
||||
|
||||
fn tcp_proxy_log_target(url: &str) -> String {
|
||||
url::Url::parse(url)
|
||||
.ok()
|
||||
.map(|parsed| {
|
||||
let mut redacted = format!("{}://", parsed.scheme());
|
||||
let Some(host) = parsed.host() else {
|
||||
return "<invalid-url>".to_owned();
|
||||
};
|
||||
redacted.push_str(&host.to_string());
|
||||
if let Some(port) = parsed.port() {
|
||||
redacted.push(':');
|
||||
redacted.push_str(&port.to_string());
|
||||
}
|
||||
redacted.push_str(parsed.path());
|
||||
redacted
|
||||
})
|
||||
.unwrap_or_else(|| "<invalid-url>".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_tcp_proxy_addr() -> String {
|
||||
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
|
||||
}
|
||||
|
||||
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
|
||||
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
|
||||
/// receives `HttpProxyResponse`.
|
||||
///
|
||||
/// The entire operation (connect + handshake + send + receive) is wrapped in
|
||||
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
|
||||
/// any stage cannot block the caller indefinitely.
|
||||
async fn tcp_proxy_request(
|
||||
method: &str,
|
||||
url: &str,
|
||||
body: &[u8],
|
||||
headers: Vec<HeaderEntry>,
|
||||
) -> ResultType<HttpProxyResponse> {
|
||||
let tcp_addr = get_tcp_proxy_addr();
|
||||
if tcp_addr.is_empty() {
|
||||
bail!("No rendezvous server configured for TCP proxy");
|
||||
}
|
||||
|
||||
let parsed = url::Url::parse(url)?;
|
||||
let path = if let Some(query) = parsed.query() {
|
||||
format!("{}?{}", parsed.path(), query)
|
||||
} else {
|
||||
parsed.path().to_string()
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"Sending {} {} via TCP proxy to {}",
|
||||
method,
|
||||
parsed.path(),
|
||||
tcp_addr
|
||||
);
|
||||
|
||||
let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
|
||||
timeout(overall_timeout, async {
|
||||
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
|
||||
let key = crate::get_key(true).await;
|
||||
secure_tcp_silent(&mut conn, &key).await?;
|
||||
|
||||
let mut req = HttpProxyRequest::new();
|
||||
req.method = method.to_uppercase();
|
||||
req.path = path;
|
||||
req.headers = headers.into();
|
||||
req.body = Bytes::from(body.to_vec());
|
||||
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_http_proxy_request(req);
|
||||
conn.send(&msg_out).await?;
|
||||
|
||||
match conn.next().await {
|
||||
Some(Ok(bytes)) => {
|
||||
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
|
||||
_ => bail!("Unexpected response from TCP proxy"),
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
|
||||
None => bail!("TCP proxy connection closed without response"),
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
|
||||
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
|
||||
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
||||
let mut entries = Vec::new();
|
||||
let mut has_content_type = false;
|
||||
if !header.is_empty() {
|
||||
let tmp: Vec<&str> = header.splitn(2, ": ").collect();
|
||||
if tmp.len() == 2 {
|
||||
if tmp[0].eq_ignore_ascii_case("Content-Type") {
|
||||
has_content_type = true;
|
||||
}
|
||||
entries.push(HeaderEntry {
|
||||
name: tmp[0].into(),
|
||||
value: tmp[1].into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
if !has_content_type {
|
||||
entries.insert(
|
||||
0,
|
||||
HeaderEntry {
|
||||
name: "Content-Type".into(),
|
||||
value: "application/json".into(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
/// POST request via TCP proxy.
|
||||
async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
|
||||
let headers = parse_simple_header(header);
|
||||
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
|
||||
if !resp.error.is_empty() {
|
||||
bail!("TCP proxy error: {}", resp.error);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&resp.body).to_string())
|
||||
}
|
||||
|
||||
fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
|
||||
if !resp.error.is_empty() {
|
||||
bail!("TCP proxy error: {}", resp.error);
|
||||
}
|
||||
|
||||
let mut response_headers = Map::new();
|
||||
for entry in resp.headers.iter() {
|
||||
response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
|
||||
}
|
||||
|
||||
let mut result = Map::new();
|
||||
result.insert("status_code".to_string(), json!(resp.status));
|
||||
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||
result.insert(
|
||||
"body".to_string(),
|
||||
json!(String::from_utf8_lossy(&resp.body)),
|
||||
);
|
||||
|
||||
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
||||
}
|
||||
|
||||
fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
|
||||
let v: Value = serde_json::from_str(header)?;
|
||||
if let Value::Object(obj) = v {
|
||||
Ok(obj
|
||||
.iter()
|
||||
.map(|(key, value)| HeaderEntry {
|
||||
name: key.clone(),
|
||||
value: value.as_str().unwrap_or_default().into(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
Err(anyhow!("HTTP header information parsing failed!"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
|
||||
async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = post_request_(
|
||||
&url,
|
||||
url,
|
||||
tls_url,
|
||||
body.clone(),
|
||||
body.to_owned(),
|
||||
header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?;
|
||||
Ok(response.text().await?)
|
||||
let status = response.status().as_u16();
|
||||
let text = response.text().await?;
|
||||
Ok((status, text))
|
||||
}
|
||||
|
||||
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
|
||||
/// if the URL is eligible. 4xx responses are returned as-is.
|
||||
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
|
||||
url: &str,
|
||||
method: &str,
|
||||
http_fn: HttpFut,
|
||||
tcp_fn: TcpFut,
|
||||
) -> ResultType<String>
|
||||
where
|
||||
HttpFut: Future<Output = ResultType<(u16, String)>>,
|
||||
TcpFut: Future<Output = ResultType<String>>,
|
||||
{
|
||||
if should_use_raw_tcp_for_api(url) {
|
||||
return tcp_fn.await;
|
||||
}
|
||||
|
||||
let http_result = http_fn.await;
|
||||
let should_fallback = match &http_result {
|
||||
Err(_) => true,
|
||||
Ok((status, _)) => *status >= 500,
|
||||
};
|
||||
|
||||
if should_fallback && can_fallback_to_raw_tcp(url) {
|
||||
log::warn!(
|
||||
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
|
||||
method,
|
||||
tcp_proxy_log_target(url),
|
||||
http_result
|
||||
.as_ref()
|
||||
.map(|(s, _)| *s)
|
||||
.map_err(|e| e.to_string()),
|
||||
);
|
||||
match tcp_fn.await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(tcp_err) => {
|
||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http_result.map(|(_status, text)| text)
|
||||
}
|
||||
|
||||
/// POST request with raw TCP proxy support.
|
||||
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
|
||||
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
|
||||
/// falls back to TCP proxy if WS is off.
|
||||
/// - 4xx responses are returned as-is (server is reachable, business logic error).
|
||||
/// - If fallback also fails, returns the original HTTP result (text or error).
|
||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
"POST",
|
||||
post_request_http(&url, &body, header),
|
||||
post_request_via_tcp_proxy(&url, &body, header),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -1246,21 +1511,16 @@ async fn get_http_response_async(
|
||||
tls_type.unwrap_or(TlsType::Rustls),
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
);
|
||||
let mut http_client = match method {
|
||||
let normalized_method = method.to_ascii_lowercase();
|
||||
let mut http_client = match normalized_method.as_str() {
|
||||
"get" => http_client.get(url),
|
||||
"post" => http_client.post(url),
|
||||
"put" => http_client.put(url),
|
||||
"delete" => http_client.delete(url),
|
||||
_ => return Err(anyhow!("The HTTP request method is not supported!")),
|
||||
};
|
||||
let v = serde_json::from_str(header)?;
|
||||
|
||||
if let Value::Object(obj) = v {
|
||||
for (key, value) in obj.iter() {
|
||||
http_client = http_client.header(key, value.as_str().unwrap_or_default());
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("HTTP header information parsing failed!"));
|
||||
for entry in parse_json_header_entries(header)? {
|
||||
http_client = http_client.header(entry.name, entry.value);
|
||||
}
|
||||
|
||||
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
|
||||
@@ -1340,6 +1600,51 @@ async fn get_http_response_async(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (status_code, json_string) so the caller can inspect the status
|
||||
/// without re-parsing the serialized JSON.
|
||||
async fn http_request_http(
|
||||
url: &str,
|
||||
method: &str,
|
||||
body: Option<String>,
|
||||
header: &str,
|
||||
) -> ResultType<(u16, String)> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = get_http_response_async(
|
||||
url,
|
||||
tls_url,
|
||||
method,
|
||||
body,
|
||||
header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?;
|
||||
// Serialize response headers
|
||||
let mut response_headers = Map::new();
|
||||
for (key, value) in response.headers() {
|
||||
response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
|
||||
}
|
||||
|
||||
let status_code = response.status().as_u16();
|
||||
let response_body = response.text().await?;
|
||||
|
||||
// Construct the JSON object
|
||||
let mut result = Map::new();
|
||||
result.insert("status_code".to_string(), json!(status_code));
|
||||
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||
result.insert("body".to_string(), json!(response_body));
|
||||
|
||||
// Convert map to JSON string
|
||||
let json_str = serde_json::to_string(&result)
|
||||
.map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
|
||||
Ok((status_code, json_str))
|
||||
}
|
||||
|
||||
/// HTTP request with raw TCP proxy support.
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn http_request_sync(
|
||||
url: String,
|
||||
@@ -1347,44 +1652,28 @@ pub async fn http_request_sync(
|
||||
body: Option<String>,
|
||||
header: String,
|
||||
) -> ResultType<String> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = get_http_response_async(
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
tls_url,
|
||||
&method,
|
||||
body.clone(),
|
||||
&header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
http_request_http(&url, &method, body.clone(), &header),
|
||||
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
|
||||
)
|
||||
.await?;
|
||||
// Serialize response headers
|
||||
let mut response_headers = serde_json::map::Map::new();
|
||||
for (key, value) in response.headers() {
|
||||
response_headers.insert(
|
||||
key.to_string(),
|
||||
serde_json::json!(value.to_str().unwrap_or("")),
|
||||
);
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
let status_code = response.status().as_u16();
|
||||
let response_body = response.text().await?;
|
||||
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
||||
/// Returns a JSON string with status_code, headers, body (same format as http_request_sync).
|
||||
async fn http_request_via_tcp_proxy(
|
||||
url: &str,
|
||||
method: &str,
|
||||
body: Option<&str>,
|
||||
header: &str,
|
||||
) -> ResultType<String> {
|
||||
let headers = parse_json_header_entries(header)?;
|
||||
let body_bytes = body.unwrap_or("").as_bytes();
|
||||
|
||||
// Construct the JSON object
|
||||
let mut result = serde_json::map::Map::new();
|
||||
result.insert("status_code".to_string(), serde_json::json!(status_code));
|
||||
result.insert(
|
||||
"headers".to_string(),
|
||||
serde_json::Value::Object(response_headers),
|
||||
);
|
||||
result.insert("body".to_string(), serde_json::json!(response_body));
|
||||
|
||||
// Convert map to JSON string
|
||||
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
||||
let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
|
||||
http_proxy_response_to_json(resp)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -1647,7 +1936,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
|
||||
// Skip additional encryption when using WebSocket connections (wss://)
|
||||
// as WebSocket Secure (wss://) already provides transport layer encryption.
|
||||
// This doesn't affect the end-to-end encryption between clients,
|
||||
@@ -1680,7 +1969,9 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
});
|
||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||
conn.set_key(key);
|
||||
log::info!("Connection secured");
|
||||
if log_on_success {
|
||||
log::info!("Connection secured");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1691,6 +1982,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
secure_tcp_impl(conn, key, true).await
|
||||
}
|
||||
|
||||
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
secure_tcp_impl(conn, key, false).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
|
||||
if pk.len() == 32 {
|
||||
@@ -2468,11 +2767,13 @@ mod tests {
|
||||
assert!(is_public("https://rustdesk.com/"));
|
||||
assert!(is_public("https://www.rustdesk.com/"));
|
||||
assert!(is_public("https://api.rustdesk.com/v1"));
|
||||
assert!(is_public("https://API.RUSTDESK.COM/v1"));
|
||||
assert!(is_public("https://rustdesk.com/path"));
|
||||
|
||||
// Test URLs ending with "rustdesk.com"
|
||||
assert!(is_public("rustdesk.com"));
|
||||
assert!(is_public("https://rustdesk.com"));
|
||||
assert!(is_public("https://RustDesk.com"));
|
||||
assert!(is_public("http://www.rustdesk.com"));
|
||||
assert!(is_public("https://api.rustdesk.com"));
|
||||
|
||||
@@ -2485,6 +2786,193 @@ mod tests {
|
||||
assert!(!is_public("rustdesk.comhello.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_use_tcp_proxy_for_api_url() {
|
||||
assert!(should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com/api/login",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com:21114/api/login",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://api.telegram.org/bot123/sendMessage",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.rustdesk.com/api/login",
|
||||
"https://admin.rustdesk.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com/api/login",
|
||||
"not a url"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"not a url",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
|
||||
struct RestoreCustomRendezvousServer(String);
|
||||
|
||||
impl Drop for RestoreCustomRendezvousServer {
|
||||
fn drop(&mut self) {
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
self.0.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _restore = RestoreCustomRendezvousServer(Config::get_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
|
||||
));
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
"1:2".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
|
||||
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
|
||||
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("HTTP header information parsing failed!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_preserves_single_content_type() {
|
||||
let headers = parse_json_header_entries(
|
||||
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_does_not_add_default_content_type() {
|
||||
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
|
||||
|
||||
assert!(!headers
|
||||
.iter()
|
||||
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_respects_custom_content_type() {
|
||||
let headers = parse_simple_header("Content-Type: text/plain");
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_preserves_non_content_type_header() {
|
||||
let headers = parse_simple_header("Authorization: Bearer token");
|
||||
|
||||
assert!(headers.iter().any(|entry| {
|
||||
entry.name.eq_ignore_ascii_case("Authorization")
|
||||
&& entry.value.as_str() == "Bearer token"
|
||||
}));
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("application/json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_redacts_query_only() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
|
||||
"https://example.com/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
|
||||
"https://[2001:db8::1]:21114/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_proxy_response_to_json() {
|
||||
let mut resp = HttpProxyResponse {
|
||||
status: 200,
|
||||
body: br#"{"ok":true}"#.to_vec().into(),
|
||||
..Default::default()
|
||||
};
|
||||
resp.headers.push(HeaderEntry {
|
||||
name: "Content-Type".into(),
|
||||
value: "application/json".into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let json = http_proxy_response_to_json(resp).unwrap();
|
||||
let value: Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(value["status_code"], 200);
|
||||
assert_eq!(value["headers"]["content-type"], "application/json");
|
||||
assert_eq!(value["body"], r#"{"ok":true}"#);
|
||||
|
||||
let err = http_proxy_response_to_json(HttpProxyResponse {
|
||||
error: "dial failed".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("TCP proxy error: dial failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mouse_event_constants_and_mask_layout() {
|
||||
use super::input::*;
|
||||
|
||||
@@ -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(())
|
||||
@@ -1693,8 +1704,8 @@ pub fn main_get_temporary_password() -> String {
|
||||
ui_interface::temporary_password()
|
||||
}
|
||||
|
||||
pub fn main_get_permanent_password() -> String {
|
||||
ui_interface::permanent_password()
|
||||
pub fn main_set_permanent_password_with_result(password: String) -> bool {
|
||||
ui_interface::set_permanent_password_with_result(password)
|
||||
}
|
||||
|
||||
pub fn main_get_fingerprint() -> String {
|
||||
@@ -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) {
|
||||
@@ -2072,10 +2084,6 @@ pub fn main_update_temporary_password() {
|
||||
update_temporary_password();
|
||||
}
|
||||
|
||||
pub fn main_set_permanent_password(password: String) {
|
||||
set_permanent_password(password);
|
||||
}
|
||||
|
||||
pub fn main_check_super_user_permission() -> bool {
|
||||
check_super_user_permission()
|
||||
}
|
||||
@@ -2242,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())
|
||||
}
|
||||
@@ -2423,16 +2442,23 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
|
||||
}
|
||||
|
||||
pub fn is_preset_password() -> bool {
|
||||
config::HARD_SETTINGS
|
||||
let hard = config::HARD_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get("password")
|
||||
.map_or(false, |p| {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return p == &crate::ipc::get_permanent_password();
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return p == &config::Config::get_permanent_password();
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if hard.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return crate::ipc::is_permanent_password_preset();
|
||||
|
||||
// On mobile, we have no service IPC; verify against local storage.
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return config::Config::matches_permanent_password_plain(&hard);
|
||||
}
|
||||
|
||||
// Don't call this function for desktop version.
|
||||
@@ -2768,6 +2794,10 @@ pub fn main_get_common(key: String) -> String {
|
||||
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return false.to_string();
|
||||
} else if key == "permanent-password-set" {
|
||||
return ui_interface::is_permanent_password_set().to_string();
|
||||
} else if key == "local-permanent-password-set" {
|
||||
return ui_interface::is_local_permanent_password_set().to_string();
|
||||
} else {
|
||||
if key.starts_with("download-data-") {
|
||||
let id = key.replace("download-data-", "");
|
||||
@@ -2877,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() {
|
||||
@@ -3049,6 +3079,22 @@ pub mod server_side {
|
||||
return env.new_string(res).unwrap_or_default().into_raw();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
key: JString,
|
||||
) -> jstring {
|
||||
let mut env = env;
|
||||
let res = if let Ok(key) = env.get_string(&key) {
|
||||
let key: String = key.into();
|
||||
super::get_builtin_option(&key)
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
return env.new_string(res).unwrap_or_default().into_raw();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
|
||||
env: JNIEnv,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
135
src/ipc.rs
135
src/ipc.rs
@@ -632,8 +632,29 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
value = Some(Config::get_id());
|
||||
} else if name == "temporary-password" {
|
||||
value = Some(password::temporary_password());
|
||||
} else if name == "permanent-password" {
|
||||
value = Some(Config::get_permanent_password());
|
||||
} else if name == "permanent-password-storage-and-salt" {
|
||||
let (storage, salt) = Config::get_local_permanent_password_storage_and_salt();
|
||||
value = Some(storage + "\n" + &salt);
|
||||
} else if name == "permanent-password-set" {
|
||||
value = Some(if Config::has_permanent_password() {
|
||||
"Y".to_owned()
|
||||
} else {
|
||||
"N".to_owned()
|
||||
});
|
||||
} else if name == "permanent-password-is-preset" {
|
||||
let hard = config::HARD_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get("password")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let is_preset =
|
||||
!hard.is_empty() && Config::matches_permanent_password_plain(&hard);
|
||||
value = Some(if is_preset {
|
||||
"Y".to_owned()
|
||||
} else {
|
||||
"N".to_owned()
|
||||
});
|
||||
} else if name == "salt" {
|
||||
value = Some(Config::get_salt());
|
||||
} else if name == "rendezvous_server" {
|
||||
@@ -669,13 +690,24 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
allow_err!(stream.send(&Data::Config((name, value))).await);
|
||||
}
|
||||
Some(value) => {
|
||||
let mut updated = true;
|
||||
if name == "id" {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&value);
|
||||
} else if name == "temporary-password" {
|
||||
password::update_temporary_password();
|
||||
} else if name == "permanent-password" {
|
||||
Config::set_permanent_password(&value);
|
||||
if Config::is_disable_change_permanent_password() {
|
||||
log::warn!("Changing permanent password is disabled");
|
||||
updated = false;
|
||||
} else {
|
||||
Config::set_permanent_password(&value);
|
||||
}
|
||||
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
|
||||
// distinguish "accepted by daemon" vs "IPC send succeeded" without
|
||||
// reading back any secret.
|
||||
let ack = if updated { "Y" } else { "N" }.to_owned();
|
||||
allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await);
|
||||
} else if name == "salt" {
|
||||
Config::set_salt(&value);
|
||||
} else if name == "voice-call-input" {
|
||||
@@ -685,7 +717,9 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
log::info!("{} updated", name);
|
||||
if updated {
|
||||
log::info!("{} updated", name);
|
||||
}
|
||||
}
|
||||
},
|
||||
Data::Options(value) => match value {
|
||||
@@ -1143,13 +1177,57 @@ pub fn update_temporary_password() -> ResultType<()> {
|
||||
set_config("temporary-password", "".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_permanent_password() -> String {
|
||||
if let Ok(Some(v)) = get_config("permanent-password") {
|
||||
Config::set_permanent_password(&v);
|
||||
v
|
||||
} else {
|
||||
Config::get_permanent_password()
|
||||
fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> {
|
||||
let Some(payload) = payload else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some((storage, salt)) = payload.split_once('\n') else {
|
||||
bail!("Invalid permanent-password-storage-and-salt payload");
|
||||
};
|
||||
|
||||
if storage.is_empty() {
|
||||
Config::set_permanent_password_storage_for_sync("", "")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Config::set_permanent_password_storage_for_sync(storage, salt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> {
|
||||
let v = get_config("permanent-password-storage-and-salt")?;
|
||||
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||
}
|
||||
|
||||
async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> {
|
||||
let ms_timeout = 1_000;
|
||||
let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?;
|
||||
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||
}
|
||||
|
||||
pub fn is_permanent_password_set() -> bool {
|
||||
match get_config("permanent-password-set") {
|
||||
Ok(Some(v)) => {
|
||||
let v = v.trim();
|
||||
return v == "Y";
|
||||
}
|
||||
Ok(None) => {
|
||||
// No response/value (timeout).
|
||||
}
|
||||
Err(_) => {
|
||||
// Connection error.
|
||||
}
|
||||
}
|
||||
log::warn!("Failed to query permanent password state from daemon");
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_permanent_password_preset() -> bool {
|
||||
if let Ok(Some(v)) = get_config("permanent-password-is-preset") {
|
||||
let v = v.trim();
|
||||
return v == "Y";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_fingerprint() -> String {
|
||||
@@ -1159,8 +1237,41 @@ pub fn get_fingerprint() -> String {
|
||||
}
|
||||
|
||||
pub fn set_permanent_password(v: String) -> ResultType<()> {
|
||||
Config::set_permanent_password(&v);
|
||||
set_config("permanent-password", v)
|
||||
if Config::is_disable_change_permanent_password() {
|
||||
bail!("Changing permanent password is disabled");
|
||||
}
|
||||
if set_permanent_password_with_ack(v)? {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Changing permanent password was rejected by daemon");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn set_permanent_password_with_ack(v: String) -> ResultType<bool> {
|
||||
set_permanent_password_with_ack_async(v).await
|
||||
}
|
||||
|
||||
async fn set_permanent_password_with_ack_async(v: String) -> ResultType<bool> {
|
||||
// The daemon ACK/NACK is expected quickly since it applies the config in-process.
|
||||
let ms_timeout = 1_000;
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send_config("permanent-password", v).await?;
|
||||
if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? {
|
||||
if name2 == "permanent-password" {
|
||||
let v = v.trim();
|
||||
let ok = v == "Y";
|
||||
if ok {
|
||||
// Ensure the hashed permanent password storage is written to the user config file.
|
||||
// This sync must not affect the daemon ACK outcome.
|
||||
if let Err(err) = sync_permanent_password_storage_from_daemon_async().await {
|
||||
log::warn!("Failed to sync permanent password storage from daemon: {err}");
|
||||
}
|
||||
}
|
||||
return Ok(ok);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
723
src/keyboard/shortcuts.rs
Normal file
723
src/keyboard/shortcuts.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
//! 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) -> 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::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);
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "اعدادات لوحة المفاتيح"),
|
||||
("Full Access", "وصول كامل"),
|
||||
("Screen Share", "مشاركة الشاشة"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
||||
("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
||||
("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
||||
("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."),
|
||||
("JumpLink", "رابط القفز"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."),
|
||||
("Show RustDesk", "عرض RustDesk"),
|
||||
@@ -728,17 +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", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
777
src/lang/be.rs
777
src/lang/be.rs
File diff suppressed because it is too large
Load Diff
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Настройки на клавиатурата"),
|
||||
("Full Access", "Пълен достъп"),
|
||||
("Screen Share", "Споделяне на екрана"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
||||
("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
||||
("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Препратка"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
|
||||
("Show RustDesk", "Покажи RustDesk"),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Configuració del teclat"),
|
||||
("Full Access", "Accés complet"),
|
||||
("Screen Share", "Compartició de pantalla"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
||||
("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"),
|
||||
("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Marcador"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"),
|
||||
("Show RustDesk", "Mostra el RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Continua amb {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "键盘设置"),
|
||||
("Full Access", "完全访问"),
|
||||
("Screen Share", "仅共享屏幕"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
||||
("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "查看"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"),
|
||||
("Show RustDesk", "显示 RustDesk"),
|
||||
@@ -740,5 +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", "当前使用预设密码"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Nastavení klávesnice"),
|
||||
("Full Access", "Úplný přístup"),
|
||||
("Screen Share", "Sdílení obrazovky"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
||||
("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
||||
("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."),
|
||||
("Show RustDesk", "Zobrazit RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Pokračovat s {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Tastaturindstillinger"),
|
||||
("Full Access", "Fuld adgang"),
|
||||
("Screen Share", "Skærmdeling"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
||||
("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
||||
("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."),
|
||||
("Show RustDesk", "Vis RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Fortsæt med {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,9 +377,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Tastatureinstellungen"),
|
||||
("Full Access", "Vollzugriff"),
|
||||
("Screen Share", "Bildschirmfreigabe"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
||||
("JumpLink", "View"),
|
||||
("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
||||
("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."),
|
||||
("JumpLink", "Anzeigen"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."),
|
||||
("Show RustDesk", "RustDesk anzeigen"),
|
||||
("This PC", "Dieser PC"),
|
||||
@@ -740,5 +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", "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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"),
|
||||
("Full Access", "Πλήρης πρόσβαση"),
|
||||
("Screen Share", "Κοινή χρήση οθόνης"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
||||
("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
||||
("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Σύνδεσμος μετάβασης"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
|
||||
("Show RustDesk", "Εμφάνιση του RustDesk"),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Keyboard settings"),
|
||||
("Full Access", "Full access"),
|
||||
("Screen Share", "Screen share"),
|
||||
("ubuntu-21-04-required", "Wayland requires Ubuntu 21.04 or higher version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."),
|
||||
("xdp-portal-unavailable", "Wayland screen capture failed. The XDG Desktop Portal may have crashed or is unavailable. Try restarting it with `systemctl --user restart xdg-desktop-portal`."),
|
||||
("JumpLink", "View"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Please select the screen to be shared(Operate on the peer side)."),
|
||||
("One-time Password", "One-time password"),
|
||||
@@ -269,5 +272,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."),
|
||||
("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", ""),
|
||||
("Full Access", ""),
|
||||
("Screen Share", ""),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."),
|
||||
("ubuntu-21-04-required", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "View"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."),
|
||||
("Show RustDesk", ""),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Ajustes de teclado"),
|
||||
("Full Access", "Acceso completo"),
|
||||
("Screen Share", "Compartir pantalla"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."),
|
||||
("ubuntu-21-04-required", "Wayland requiere Ubuntu 21.04 o una versión superior."),
|
||||
("wayland-requires-higher-linux-version", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Ver"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."),
|
||||
("Show RustDesk", "Mostrar RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Continuar con {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Klaviatuurisätted"),
|
||||
("Full Access", "Täielik ligipääs"),
|
||||
("Screen Share", "Ekraanijagamine"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."),
|
||||
("ubuntu-21-04-required", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."),
|
||||
("wayland-requires-higher-linux-version", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."),
|
||||
("Show RustDesk", "Kuva RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Jätka koos {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Teklatuaren ezarpenak"),
|
||||
("Full Access", "Sarbide osoa"),
|
||||
("Screen Share", "Pantailaren partekatzea"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."),
|
||||
("ubuntu-21-04-required", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."),
|
||||
("wayland-requires-higher-linux-version", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Ikusi"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Mesedez, hautatu partekatuko den pantaila (Kudeatu parekidearen aldean)"),
|
||||
("Show RustDesk", "Erakutsi RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "{} honekin jarraitu"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "تنظیمات صفحه کلید"),
|
||||
("Full Access", "دسترسی کامل"),
|
||||
("Screen Share", "اشتراک گذاری صفحه"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"),
|
||||
("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"),
|
||||
("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "چشم انداز"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحهای را برای اشتراکگذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."),
|
||||
("Show RustDesk", "RustDesk نمایش"),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Näppäimistöasetukset"),
|
||||
("Full Access", "Täysi käyttöoikeus"),
|
||||
("Screen Share", "Näytönjako"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."),
|
||||
("ubuntu-21-04-required", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Pikalinkki"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Valitse jaettava näyttö (toiminto etäpäässä)."),
|
||||
("Show RustDesk", "Näytä RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Jatka käyttäen {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Paramètres du clavier"),
|
||||
("Full Access", "Accès total"),
|
||||
("Screen Share", "Partage d’écran"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."),
|
||||
("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."),
|
||||
("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."),
|
||||
("xdp-portal-unavailable", "Échec de la capture de l’écran Wayland. Le portail de bureau XDG a peut-être planté ou n’est pas disponible. Essayez de le redémarrer avec la commande `systemctl --user restart xdg-desktop-portal`."),
|
||||
("JumpLink", "Afficher"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."),
|
||||
("Show RustDesk", "Afficher RustDesk"),
|
||||
@@ -740,5 +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", "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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "კლავიატურის პარამეტრები"),
|
||||
("Full Access", "სრული წვდომა"),
|
||||
("Screen Share", "ეკრანის გაზიარება"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."),
|
||||
("ubuntu-21-04-required", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."),
|
||||
("wayland-requires-higher-linux-version", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "ნახვა"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."),
|
||||
("Show RustDesk", "RustDesk-ის ჩვენება"),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "הגדרות מקלדת"),
|
||||
("Full Access", "גישה מלאה"),
|
||||
("Screen Share", "שיתוף מסך"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"),
|
||||
("ubuntu-21-04-required", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"),
|
||||
("wayland-requires-higher-linux-version", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "קישור מהיר"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."),
|
||||
("Show RustDesk", "הצג את RustDesk"),
|
||||
@@ -740,5 +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", ""),
|
||||
("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();
|
||||
}
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Postavke tipkovnice"),
|
||||
("Full Access", "Potpuni pristup"),
|
||||
("Screen Share", "Dijeljenje zaslona"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."),
|
||||
("ubuntu-21-04-required", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"),
|
||||
("wayland-requires-higher-linux-version", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Vidi"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Molimo odaberite zaslon koji će biti podijeljen (Za rad na strani klijenta)"),
|
||||
("Show RustDesk", "Prikaži RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Nastavi sa {}"),
|
||||
("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"),
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Billentyűzetbeállítások"),
|
||||
("Full Access", "Teljes hozzáférés"),
|
||||
("Screen Share", "Képernyőmegosztás"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."),
|
||||
("ubuntu-21-04-required", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."),
|
||||
("wayland-requires-higher-linux-version", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Hiperhivatkozás"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."),
|
||||
("Show RustDesk", "A RustDesk megjelenítése"),
|
||||
@@ -407,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"),
|
||||
@@ -441,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"),
|
||||
@@ -558,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"),
|
||||
@@ -621,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"),
|
||||
@@ -642,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"),
|
||||
@@ -681,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"),
|
||||
@@ -740,5 +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", "Á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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Pengaturan Papan Ketik"),
|
||||
("Full Access", "Akses penuh"),
|
||||
("Screen Share", "Berbagi Layar"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."),
|
||||
("ubuntu-21-04-required", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."),
|
||||
("wayland-requires-higher-linux-version", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Tautan Cepat"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan kepada rekan anda."),
|
||||
("Show RustDesk", "Tampilkan RustDesk"),
|
||||
@@ -740,5 +741,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Lanjutkan dengan {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Impostazioni tastiera"),
|
||||
("Full Access", "Accesso completo"),
|
||||
("Screen Share", "Condivisione schermo"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."),
|
||||
("ubuntu-21-04-required", "Wayland richiede Ubuntu 21.04 o versione successiva."),
|
||||
("wayland-requires-higher-linux-version", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."),
|
||||
("xdp-portal-unavailable", "Acquisizione dello schermo di Wayland non riuscita. Il portale desktop XDG potrebbe essersi bloccato o non essere disponibile. Prova a riavviarlo con `systemctl --user restart xdg-desktop-portal`."),
|
||||
("JumpLink", "Vai a"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato dispositivo remoto)."),
|
||||
("Show RustDesk", "Visualizza RustDesk"),
|
||||
@@ -740,5 +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", "È 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();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user