mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-23 22:18:40 +03:00
Merge branch 'master' into terminal-utf8-and-reconnect
This commit is contained in:
120
AGENTS.md
120
AGENTS.md
@@ -1,47 +1,18 @@
|
|||||||
# RustDesk Guide
|
# RustDesk Guide
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
## Project Layout
|
||||||
|
|
||||||
## 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
|
### Directory Structure
|
||||||
- **`src/`** - Main Rust application code
|
* `src/` Rust app
|
||||||
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
|
* `src/server/` audio / clipboard / input / video / network
|
||||||
- `src/server/` - Audio/clipboard/input/video services and network connections
|
* `src/platform/` platform-specific code
|
||||||
- `src/client.rs` - Peer connection handling
|
* `src/ui/` legacy Sciter UI (deprecated)
|
||||||
- `src/platform/` - Platform-specific code
|
* `flutter/` current UI
|
||||||
- **`flutter/`** - Flutter UI code for desktop and mobile
|
* `libs/hbb_common/` config / proto / shared utils
|
||||||
- **`libs/`** - Core libraries
|
* `libs/scrap/` screen capture
|
||||||
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
|
* `libs/enigo/` input control
|
||||||
- `libs/scrap/` - Screen capture functionality
|
* `libs/clipboard/` clipboard
|
||||||
- `libs/enigo/` - Platform-specific keyboard/mouse control
|
* `libs/hbb_common/src/config.rs` all options
|
||||||
- `libs/clipboard/` - Cross-platform clipboard implementation
|
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
|
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
|
||||||
@@ -57,50 +28,35 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- Mobile: `flutter/lib/mobile/`
|
- Mobile: `flutter/lib/mobile/`
|
||||||
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
|
- 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
|
|
||||||
|
|
||||||
## Rust Rules
|
## Rust Rules
|
||||||
|
|
||||||
- In Rust code, do not introduce `unwrap()` or `expect()`.
|
* Avoid `unwrap()` / `expect()` in production code.
|
||||||
- Allowed exceptions:
|
* Exceptions:
|
||||||
- Tests may use `unwrap()` or `expect()` when it keeps the test focused and readable.
|
|
||||||
- Lock acquisition may use `unwrap()` only when the locking API makes that the practical option and the failure mode is poison handling rather than normal control flow.
|
* tests;
|
||||||
- Outside those exceptions, propagate errors, handle them explicitly, or use safer fallbacks instead of `unwrap()` and `expect()`.
|
* 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.
|
||||||
|
|
||||||
## Editing Hygiene
|
## Editing Hygiene
|
||||||
|
|
||||||
- Do not introduce formatting-only changes.
|
* Change only what is required.
|
||||||
- Do not run repository-wide formatters or reflow unrelated code unless the
|
* Prefer the smallest valid diff.
|
||||||
user explicitly asks for formatting.
|
* Do not refactor unrelated code.
|
||||||
- Keep diffs limited to semantic changes required for the task.
|
* Do not make formatting-only changes.
|
||||||
|
* Keep naming/style consistent with nearby code.
|
||||||
|
|||||||
2
build.py
2
build.py
@@ -512,7 +512,7 @@ def main():
|
|||||||
system2('pip3 install -r requirements.txt')
|
system2('pip3 install -r requirements.txt')
|
||||||
system2(
|
system2(
|
||||||
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
|
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'):
|
elif os.path.isfile('/usr/bin/pacman'):
|
||||||
# pacman -S -needed base-devel
|
# pacman -S -needed base-devel
|
||||||
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
|
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`.
|
- 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
|
- 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
|
## Comment compiler/build sous Linux
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ cd rustdesk
|
|||||||
mkdir -p target/debug
|
mkdir -p target/debug
|
||||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||||
mv libsciter-gtk.so target/debug
|
mv libsciter-gtk.so target/debug
|
||||||
Exécution du cargo
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comment construire avec Docker
|
## 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é.
|
||||||
@@ -62,7 +62,13 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
return false
|
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")
|
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,7 +532,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Official
|
// Official
|
||||||
TapGestureRecognizer:
|
TapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
() => TapGestureRecognizer(), (instance) {
|
() => TapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onTapDown = onTapDown
|
..onTapDown = onTapDown
|
||||||
..onTapUp = onTapUp
|
..onTapUp = onTapUp
|
||||||
@@ -540,14 +542,18 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}),
|
}),
|
||||||
DoubleTapGestureRecognizer:
|
DoubleTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||||
() => DoubleTapGestureRecognizer(), (instance) {
|
() => DoubleTapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onDoubleTapDown = onDoubleTapDown
|
..onDoubleTapDown = onDoubleTapDown
|
||||||
..onDoubleTap = onDoubleTap;
|
..onDoubleTap = onDoubleTap;
|
||||||
}),
|
}),
|
||||||
LongPressGestureRecognizer:
|
LongPressGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||||
() => LongPressGestureRecognizer(), (instance) {
|
() => LongPressGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onLongPressDown = onLongPressDown
|
..onLongPressDown = onLongPressDown
|
||||||
..onLongPressUp = onLongPressUp
|
..onLongPressUp = onLongPressUp
|
||||||
@@ -557,7 +563,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Customized
|
// Customized
|
||||||
HoldTapMoveGestureRecognizer:
|
HoldTapMoveGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||||
() => HoldTapMoveGestureRecognizer(),
|
() => HoldTapMoveGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
),
|
||||||
(instance) => instance
|
(instance) => instance
|
||||||
..onHoldDragStart = onHoldDragStart
|
..onHoldDragStart = onHoldDragStart
|
||||||
..onHoldDragUpdate = onHoldDragUpdate
|
..onHoldDragUpdate = onHoldDragUpdate
|
||||||
@@ -565,14 +573,18 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
..onHoldDragEnd = onHoldDragEnd),
|
..onHoldDragEnd = onHoldDragEnd),
|
||||||
DoubleFinerTapGestureRecognizer:
|
DoubleFinerTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
||||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
() => DoubleFinerTapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onDoubleFinerTap = onDoubleFinerTap
|
..onDoubleFinerTap = onDoubleFinerTap
|
||||||
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
||||||
}),
|
}),
|
||||||
CustomTouchGestureRecognizer:
|
CustomTouchGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||||
() => CustomTouchGestureRecognizer(), (instance) {
|
() => CustomTouchGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance.onOneFingerPanStart =
|
instance.onOneFingerPanStart =
|
||||||
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
||||||
instance
|
instance
|
||||||
|
|||||||
@@ -759,9 +759,18 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false;
|
||||||
|
|
||||||
|
// Backend revocation already attempts to turn privacy mode off.
|
||||||
|
// Still keep this menu when privacy mode is active, so users can turn it off
|
||||||
|
// if there is a sync delay, version mismatch, or off attempt failure.
|
||||||
|
if (!hasPrivacyModePermission && privacyModeState.isEmpty) {
|
||||||
|
return []; // No permission and not active, hide options.
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
||||||
final enabled = !ffi.ffiModel.viewOnly;
|
final enabled =
|
||||||
|
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
value: privacyModeState.isNotEmpty,
|
value: privacyModeState.isNotEmpty,
|
||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
@@ -810,18 +819,29 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return privacyModeImpls.map((e) {
|
final visibleImpls = hasPrivacyModePermission
|
||||||
|
? privacyModeImpls
|
||||||
|
: privacyModeImpls.where((e) {
|
||||||
|
final implKey = (e as List<dynamic>)[0] as String;
|
||||||
|
return privacyModeState.value == implKey;
|
||||||
|
}).toList();
|
||||||
|
return visibleImpls.map((e) {
|
||||||
final implKey = (e as List<dynamic>)[0] as String;
|
final implKey = (e as List<dynamic>)[0] as String;
|
||||||
final implName = (e)[1] as String;
|
final implName = (e)[1] as String;
|
||||||
|
final enabled = !ffiModel.viewOnly &&
|
||||||
|
(hasPrivacyModePermission || privacyModeState.value == implKey);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
child: Text(translate(implName)),
|
child: Text(translate(implName)),
|
||||||
value: privacyModeState.value == implKey,
|
value: privacyModeState.value == implKey,
|
||||||
onChanged: (value) {
|
onChanged: enabled
|
||||||
if (value == null) return;
|
? (value) {
|
||||||
togglePrivacyModeTime = DateTime.now();
|
if (value == null) return;
|
||||||
bind.sessionTogglePrivacyMode(
|
if (value && !hasPrivacyModePermission) return;
|
||||||
sessionId: sessionId, implKey: implKey, on: value);
|
togglePrivacyModeTime = DateTime.now();
|
||||||
});
|
bind.sessionTogglePrivacyMode(
|
||||||
|
sessionId: sessionId, implKey: implKey, on: value);
|
||||||
|
}
|
||||||
|
: null);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ const String kOptionTerminalPersistent = "terminal-persistent";
|
|||||||
const String kOptionEnableTunnel = "enable-tunnel";
|
const String kOptionEnableTunnel = "enable-tunnel";
|
||||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||||
const String kOptionEnableBlockInput = "enable-block-input";
|
const String kOptionEnableBlockInput = "enable-block-input";
|
||||||
|
const String kOptionEnablePrivacyMode = "enable-privacy-mode";
|
||||||
|
const String kOptionEnablePermChangeInAcceptWindow =
|
||||||
|
"enable-perm-change-in-accept-window";
|
||||||
const String kOptionAllowRemoteConfigModification =
|
const String kOptionAllowRemoteConfigModification =
|
||||||
"allow-remote-config-modification";
|
"allow-remote-config-modification";
|
||||||
const String kOptionVerificationMethod = "verification-method";
|
const String kOptionVerificationMethod = "verification-method";
|
||||||
|
|||||||
@@ -1062,6 +1062,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
_OptionCheckBox(context, 'Enable blocking user input',
|
_OptionCheckBox(context, 'Enable blocking user input',
|
||||||
kOptionEnableBlockInput,
|
kOptionEnableBlockInput,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
|
if (bind.mainSupportedPrivacyModeImpls() != '[]')
|
||||||
|
_OptionCheckBox(
|
||||||
|
context, 'Enable privacy mode', kOptionEnablePrivacyMode,
|
||||||
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(context, 'Enable remote configuration modification',
|
_OptionCheckBox(context, 'Enable remote configuration modification',
|
||||||
kOptionAllowRemoteConfigModification,
|
kOptionAllowRemoteConfigModification,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
|
|||||||
@@ -610,19 +610,24 @@ class _PrivilegeBoard extends StatefulWidget {
|
|||||||
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||||
late final client = widget.client;
|
late final client = widget.client;
|
||||||
Widget buildPermissionIcon(bool enabled, IconData iconData,
|
Widget buildPermissionIcon(bool enabled, IconData iconData,
|
||||||
Function(bool)? onTap, String tooltipText) {
|
Function(bool)? onTap, String tooltipText,
|
||||||
|
{required bool canModify}) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
||||||
waitDuration: Duration.zero,
|
waitDuration: Duration.zero,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? MyTheme.accent : Colors.grey[700],
|
color: enabled
|
||||||
|
? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6))
|
||||||
|
: Colors.grey[700],
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () =>
|
onTap: canModify
|
||||||
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
|
? () =>
|
||||||
|
checkClickTime(widget.client.id, () => onTap?.call(!enabled))
|
||||||
|
: null,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
@@ -643,6 +648,9 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final crossAxisCount = 4;
|
final crossAxisCount = 4;
|
||||||
final spacing = 10.0;
|
final spacing = 10.0;
|
||||||
|
final canModifyPermission =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) !=
|
||||||
|
'N';
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 160.0,
|
height: 160.0,
|
||||||
@@ -689,6 +697,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable audio'),
|
translate('Enable audio'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.recording,
|
client.recording,
|
||||||
@@ -703,6 +712,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable recording session'),
|
translate('Enable recording session'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -719,6 +729,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable keyboard/mouse'),
|
translate('Enable keyboard/mouse'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.clipboard,
|
client.clipboard,
|
||||||
@@ -733,6 +744,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable clipboard'),
|
translate('Enable clipboard'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.audio,
|
client.audio,
|
||||||
@@ -747,6 +759,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable audio'),
|
translate('Enable audio'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.file,
|
client.file,
|
||||||
@@ -761,6 +774,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable file copy and paste'),
|
translate('Enable file copy and paste'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.restart,
|
client.restart,
|
||||||
@@ -775,6 +789,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable remote restart'),
|
translate('Enable remote restart'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.recording,
|
client.recording,
|
||||||
@@ -789,6 +804,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable recording session'),
|
translate('Enable recording session'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
// only windows support block input
|
// only windows support block input
|
||||||
if (isWindows)
|
if (isWindows)
|
||||||
@@ -805,6 +821,23 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable blocking user input'),
|
translate('Enable blocking user input'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
|
),
|
||||||
|
if (bind.mainSupportedPrivacyModeImpls() != '[]')
|
||||||
|
buildPermissionIcon(
|
||||||
|
client.privacyMode,
|
||||||
|
Icons.visibility_off,
|
||||||
|
(enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id,
|
||||||
|
name: "privacy_mode",
|
||||||
|
enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.privacyMode = enabled;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
translate('Enable privacy mode'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -996,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
toggles(),
|
toggles(),
|
||||||
];
|
];
|
||||||
// privacy mode
|
// privacy mode
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (ffi.connType == ConnType.defaultConn &&
|
if (ffi.connType == ConnType.defaultConn &&
|
||||||
ffiModel.keyboard &&
|
(pi.features.privacyMode || privacyModeState.isNotEmpty) &&
|
||||||
pi.features.privacyMode) {
|
(ffiModel.keyboard || privacyModeState.isNotEmpty)) {
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
|
||||||
final privacyModeList =
|
final privacyModeList =
|
||||||
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
||||||
if (privacyModeList.length == 1) {
|
if (privacyModeList.length == 1) {
|
||||||
|
|||||||
@@ -426,12 +426,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: inputModel.isPhysicalMouse.value
|
child: RawTouchGestureDetectorRegion(
|
||||||
? getBodyForMobile()
|
child: getBodyForMobile(),
|
||||||
: RawTouchGestureDetectorRegion(
|
ffi: gFFI,
|
||||||
child: getBodyForMobile(),
|
),
|
||||||
ffi: gFFI,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1185,7 +1183,8 @@ void showOptions(
|
|||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
// privacy mode
|
// privacy mode
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
|
privacyModeState.isNotEmpty) {
|
||||||
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
||||||
if (privacyModeList.length == 1) {
|
if (privacyModeList.length == 1) {
|
||||||
displayToggles.add(privacyModeList[0]);
|
displayToggles.add(privacyModeList[0]);
|
||||||
|
|||||||
@@ -583,9 +583,16 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
final hasAudioPermission = androidVersion >= 30;
|
final hasAudioPermission = androidVersion >= 30;
|
||||||
final hideStopService =
|
final hideStopService = isAndroid &&
|
||||||
isAndroid &&
|
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||||
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
final allowPermChangeInAcceptWindow = option2bool(
|
||||||
|
kOptionEnablePermChangeInAcceptWindow,
|
||||||
|
bind.mainGetBuildinOption(
|
||||||
|
key: kOptionEnablePermChangeInAcceptWindow,
|
||||||
|
));
|
||||||
|
final permissionChangeLocked = isAndroid &&
|
||||||
|
serverModel.clients.any((c) => !c.disconnected) &&
|
||||||
|
!allowPermChangeInAcceptWindow;
|
||||||
return PaddingCard(
|
return PaddingCard(
|
||||||
title: translate("Permissions"),
|
title: translate("Permissions"),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
@@ -608,13 +615,21 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||||
? () => showScamWarning(context, serverModel)
|
? () => showScamWarning(context, serverModel)
|
||||||
: serverModel.toggleService),
|
: serverModel.toggleService),
|
||||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
PermissionRow(
|
||||||
serverModel.toggleInput),
|
translate("Input Control"),
|
||||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
serverModel.inputOk,
|
||||||
serverModel.toggleFile),
|
serverModel.toggleInput,
|
||||||
|
),
|
||||||
|
PermissionRow(
|
||||||
|
translate("Transfer file"),
|
||||||
|
serverModel.fileOk,
|
||||||
|
serverModel.toggleFile,
|
||||||
|
enabled: !permissionChangeLocked,
|
||||||
|
),
|
||||||
hasAudioPermission
|
hasAudioPermission
|
||||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||||
serverModel.toggleAudio)
|
serverModel.toggleAudio,
|
||||||
|
enabled: !permissionChangeLocked)
|
||||||
: Row(children: [
|
: Row(children: [
|
||||||
Icon(Icons.info_outline).marginOnly(right: 15),
|
Icon(Icons.info_outline).marginOnly(right: 15),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -623,19 +638,25 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
style: const TextStyle(color: MyTheme.darkGray),
|
style: const TextStyle(color: MyTheme.darkGray),
|
||||||
))
|
))
|
||||||
]),
|
]),
|
||||||
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
|
PermissionRow(
|
||||||
serverModel.toggleClipboard),
|
translate("Enable clipboard"),
|
||||||
|
serverModel.clipboardOk,
|
||||||
|
serverModel.toggleClipboard,
|
||||||
|
enabled: !permissionChangeLocked,
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PermissionRow extends StatelessWidget {
|
class PermissionRow extends StatelessWidget {
|
||||||
const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
|
const PermissionRow(this.name, this.isOk, this.onPressed,
|
||||||
|
{Key? key, this.enabled = true})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final bool isOk;
|
final bool isOk;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -644,9 +665,11 @@ class PermissionRow extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.all(0),
|
contentPadding: EdgeInsets.all(0),
|
||||||
title: Text(name),
|
title: Text(name),
|
||||||
value: isOk,
|
value: isOk,
|
||||||
onChanged: (bool value) {
|
onChanged: enabled
|
||||||
onPressed();
|
? (bool value) {
|
||||||
});
|
onPressed();
|
||||||
|
}
|
||||||
|
: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,13 +259,11 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: inputModel.isPhysicalMouse.value
|
child: RawTouchGestureDetectorRegion(
|
||||||
? getBodyForMobile()
|
child: getBodyForMobile(),
|
||||||
: RawTouchGestureDetectorRegion(
|
ffi: gFFI,
|
||||||
child: getBodyForMobile(),
|
isCamera: true,
|
||||||
ffi: gFFI,
|
),
|
||||||
isCamera: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
@@ -15,12 +16,13 @@ import 'package:get/get.dart';
|
|||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../models/state_model.dart';
|
import '../../models/state_model.dart';
|
||||||
|
import 'input_modifier_utils.dart';
|
||||||
import 'relative_mouse_model.dart';
|
import 'relative_mouse_model.dart';
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
/// Mouse button enum.
|
/// Mouse button enum.
|
||||||
enum MouseButtons { left, right, wheel, back }
|
enum MouseButtons { left, right, wheel, back, forward }
|
||||||
|
|
||||||
const _kMouseEventDown = 'mousedown';
|
const _kMouseEventDown = 'mousedown';
|
||||||
const _kMouseEventUp = 'mouseup';
|
const _kMouseEventUp = 'mouseup';
|
||||||
@@ -157,6 +159,8 @@ extension ToString on MouseButtons {
|
|||||||
return 'wheel';
|
return 'wheel';
|
||||||
case MouseButtons.back:
|
case MouseButtons.back:
|
||||||
return 'back';
|
return 'back';
|
||||||
|
case MouseButtons.forward:
|
||||||
|
return 'forward';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,6 +331,80 @@ class ToReleaseKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InputModel {
|
class InputModel {
|
||||||
|
// Side mouse button support for Linux.
|
||||||
|
// Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
|
||||||
|
// natively via GDK and forward through the platform channel.
|
||||||
|
static InputModel? _activeSideButtonModel;
|
||||||
|
// Tracks per-button which model received a side button down event, so the
|
||||||
|
// matching up event is routed there even if the pointer has left the view
|
||||||
|
// or a different button was pressed in between.
|
||||||
|
static final Map<MouseButtons, InputModel> _sideButtonDownModels = {};
|
||||||
|
static bool _sideButtonChannelInitialized = false;
|
||||||
|
|
||||||
|
/// Each Flutter engine (main window + sub-windows from desktop_multi_window)
|
||||||
|
/// runs its own Dart isolate with its own statics. Called from initEnv()
|
||||||
|
/// which runs per-engine, so each isolate registers its own handler tied
|
||||||
|
/// to its own set of InputModels.
|
||||||
|
static void initSideButtonChannel() {
|
||||||
|
if (!Platform.isLinux) return;
|
||||||
|
if (_sideButtonChannelInitialized) return;
|
||||||
|
_sideButtonChannelInitialized = true;
|
||||||
|
|
||||||
|
const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
|
||||||
|
channel.setMethodCallHandler((call) async {
|
||||||
|
if (call.method == 'onSideMouseButton') {
|
||||||
|
final args = call.arguments as Map<dynamic, dynamic>;
|
||||||
|
final button = args['button'] as String;
|
||||||
|
final type = args['type'] as String;
|
||||||
|
final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
|
||||||
|
|
||||||
|
if (type == 'down') {
|
||||||
|
final model = _activeSideButtonModel;
|
||||||
|
if (model != null &&
|
||||||
|
!(model.isViewOnly && !model.showMyCursor) &&
|
||||||
|
model.keyboardPerm &&
|
||||||
|
!model.isViewCamera) {
|
||||||
|
_sideButtonDownModels[mb] = model;
|
||||||
|
// Fire-and-forget to avoid blocking the platform channel handler.
|
||||||
|
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only route 'up' when we recorded the matching 'down';
|
||||||
|
// dropping avoids sending unpaired 'up' to an unrelated session.
|
||||||
|
// Use _sendMouseUnchecked to bypass permission checks so the
|
||||||
|
// release always goes through even if permissions changed.
|
||||||
|
final model = _sideButtonDownModels.remove(mb);
|
||||||
|
if (model != null) {
|
||||||
|
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear any static references to this model (prevents stale routing).
|
||||||
|
/// Releases any held side buttons on the peer so closing a session
|
||||||
|
/// mid-press does not leave a stuck button.
|
||||||
|
void disposeSideButtonTracking() {
|
||||||
|
if (_activeSideButtonModel == this) _activeSideButtonModel = null;
|
||||||
|
final held = _sideButtonDownModels.entries
|
||||||
|
.where((e) => e.value == this)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
for (final mb in held) {
|
||||||
|
_sideButtonDownModels.remove(mb);
|
||||||
|
// Best-effort release; session may already be tearing down.
|
||||||
|
unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to release side button $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
String keyboardMode = '';
|
String keyboardMode = '';
|
||||||
|
|
||||||
@@ -412,6 +490,7 @@ class InputModel {
|
|||||||
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
||||||
|
|
||||||
InputModel(this.parent) {
|
InputModel(this.parent) {
|
||||||
|
initSideButtonChannel();
|
||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
_relativeMouse = RelativeMouseModel(
|
_relativeMouse = RelativeMouseModel(
|
||||||
sessionId: sessionId,
|
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) {
|
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||||
if (isViewOnly) return KeyEventResult.handled;
|
if (isViewOnly) return KeyEventResult.handled;
|
||||||
if (isViewCamera) return KeyEventResult.handled;
|
if (isViewCamera) return KeyEventResult.handled;
|
||||||
@@ -674,6 +785,27 @@ class InputModel {
|
|||||||
toReleaseRawKeys.updateKeyUp(key, e);
|
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
|
// * Currently mobile does not enable map mode
|
||||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||||
mapKeyboardModeRaw(e, iosCapsLock);
|
mapKeyboardModeRaw(e, iosCapsLock);
|
||||||
@@ -717,6 +849,8 @@ class InputModel {
|
|||||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
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) {
|
if (e is KeyUpEvent) {
|
||||||
handleKeyUpEventModifiers(e);
|
handleKeyUpEventModifiers(e);
|
||||||
} else if (e is KeyDownEvent) {
|
} 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 =
|
final isDesktopAndMapMode =
|
||||||
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
||||||
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
||||||
@@ -966,13 +1115,20 @@ class InputModel {
|
|||||||
return evt;
|
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.
|
/// Send mouse press event.
|
||||||
Future<void> sendMouse(String type, MouseButtons button) async {
|
Future<void> sendMouse(String type, MouseButtons button) async {
|
||||||
if (!keyboardPerm) return;
|
if (!keyboardPerm) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
await bind.sessionSendMouse(
|
await _sendMouseUnchecked(type, button);
|
||||||
sessionId: sessionId,
|
|
||||||
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterOrLeave(bool enter) {
|
void enterOrLeave(bool enter) {
|
||||||
@@ -982,6 +1138,13 @@ class InputModel {
|
|||||||
_pointerInsideImage = enter;
|
_pointerInsideImage = enter;
|
||||||
_lastWheelTsUs = 0;
|
_lastWheelTsUs = 0;
|
||||||
|
|
||||||
|
// Track active model for side button events (Linux).
|
||||||
|
if (enter) {
|
||||||
|
_activeSideButtonModel = this;
|
||||||
|
} else if (_activeSideButtonModel == this) {
|
||||||
|
_activeSideButtonModel = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Fix status
|
// Fix status
|
||||||
if (!enter) {
|
if (!enter) {
|
||||||
resetModifiers();
|
resetModifiers();
|
||||||
@@ -1332,6 +1495,16 @@ class InputModel {
|
|||||||
return false;
|
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) {
|
void onPointDownImage(PointerDownEvent e) {
|
||||||
debugPrint("onPointDownImage ${e.kind}");
|
debugPrint("onPointDownImage ${e.kind}");
|
||||||
_stopFling = true;
|
_stopFling = true;
|
||||||
@@ -1344,6 +1517,9 @@ class InputModel {
|
|||||||
// Track mouse down events for duplicate detection on iOS.
|
// Track mouse down events for duplicate detection on iOS.
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||||
if (e.kind == ui.PointerDeviceKind.mouse) {
|
if (e.kind == ui.PointerDeviceKind.mouse) {
|
||||||
|
if (!isPhysicalMouse.value) {
|
||||||
|
isPhysicalMouse.value = true;
|
||||||
|
}
|
||||||
_lastMouseDownTimeMs = nowMs;
|
_lastMouseDownTimeMs = nowMs;
|
||||||
_lastMouseDownPos = e.position;
|
_lastMouseDownPos = e.position;
|
||||||
}
|
}
|
||||||
@@ -1353,6 +1529,10 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
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) {
|
if (isPhysicalMouse.value) {
|
||||||
isPhysicalMouse.value = false;
|
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;
|
||||||
|
}
|
||||||
@@ -3932,6 +3932,7 @@ class FFI {
|
|||||||
inputModel.resetModifiers();
|
inputModel.resetModifiers();
|
||||||
// Dispose relative mouse mode resources to ensure cursor is restored
|
// Dispose relative mouse mode resources to ensure cursor is restored
|
||||||
inputModel.disposeRelativeMouseMode();
|
inputModel.disposeRelativeMouseMode();
|
||||||
|
inputModel.disposeSideButtonTracking();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
await bind.sessionClose(sessionId: sessionId);
|
await bind.sessionClose(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAudio() async {
|
toggleAudio() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
||||||
@@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleFile() async {
|
toggleFile() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (!_fileOk &&
|
if (!_fileOk &&
|
||||||
@@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleInput() async {
|
toggleInput() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
@@ -549,10 +549,19 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
_clients.add(client);
|
_clients.add(client);
|
||||||
} else {
|
} else {
|
||||||
|
if (_clients[index].authorized) {
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_clients[index].authorized = true;
|
_clients[index].authorized = true;
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_clients.any((c) => c.id == client.id)) {
|
final index = _clients.indexWhere((c) => c.id == client.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_clients.add(client);
|
_clients.add(client);
|
||||||
@@ -818,6 +827,7 @@ class Client {
|
|||||||
bool restart = false;
|
bool restart = false;
|
||||||
bool recording = false;
|
bool recording = false;
|
||||||
bool blockInput = false;
|
bool blockInput = false;
|
||||||
|
bool privacyMode = false;
|
||||||
bool disconnected = false;
|
bool disconnected = false;
|
||||||
bool fromSwitch = false;
|
bool fromSwitch = false;
|
||||||
bool inVoiceCall = false;
|
bool inVoiceCall = false;
|
||||||
@@ -846,6 +856,7 @@ class Client {
|
|||||||
restart = json['restart'];
|
restart = json['restart'];
|
||||||
recording = json['recording'];
|
recording = json['recording'];
|
||||||
blockInput = json['block_input'];
|
blockInput = json['block_input'];
|
||||||
|
privacyMode = json['privacy_mode'] ?? privacyMode;
|
||||||
disconnected = json['disconnected'];
|
disconnected = json['disconnected'];
|
||||||
fromSwitch = json['from_switch'];
|
fromSwitch = json['from_switch'];
|
||||||
inVoiceCall = json['in_voice_call'];
|
inVoiceCall = json['in_voice_call'];
|
||||||
@@ -870,6 +881,7 @@ class Client {
|
|||||||
data['restart'] = restart;
|
data['restart'] = restart;
|
||||||
data['recording'] = recording;
|
data['recording'] = recording;
|
||||||
data['block_input'] = blockInput;
|
data['block_input'] = blockInput;
|
||||||
|
data['privacy_mode'] = privacyMode;
|
||||||
data['disconnected'] = disconnected;
|
data['disconnected'] = disconnected;
|
||||||
data['from_switch'] = fromSwitch;
|
data['from_switch'] = fromSwitch;
|
||||||
data['in_voice_call'] = inVoiceCall;
|
data['in_voice_call'] = inVoiceCall;
|
||||||
|
|||||||
@@ -1729,7 +1729,7 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String mainSupportedPrivacyModeImpls({dynamic hint}) {
|
String mainSupportedPrivacyModeImpls({dynamic hint}) {
|
||||||
throw UnimplementedError("mainSupportedPrivacyModeImpls");
|
return '[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
String mainSupportedInputSource({dynamic hint}) {
|
String mainSupportedInputSource({dynamic hint}) {
|
||||||
|
|||||||
@@ -29,6 +29,80 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
|
|||||||
|
|
||||||
extern bool gIsConnectionManager;
|
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);
|
GtkWidget *find_gl_area(GtkWidget *widget);
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
// 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(window));
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
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.
|
||||||
// Register callback for sub-windows created by desktop_multi_window plugin
|
// Handles both Wayland shortcuts inhibition (guarded inside) and side button
|
||||||
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
|
// 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(
|
desktop_multi_window_plugin_set_window_created_callback(
|
||||||
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
|
(WindowCreatedCallback)on_subwindow_created);
|
||||||
#endif
|
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
@@ -116,6 +190,11 @@ static void my_application_activate(GApplication* application) {
|
|||||||
self,
|
self,
|
||||||
nullptr);
|
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));
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
icons_launcher: ^2.0.4
|
icons_launcher: ^2.0.4
|
||||||
#flutter_test:
|
flutter_test:
|
||||||
#sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.4.6
|
build_runner: ^2.4.6
|
||||||
freezed: ^2.4.2
|
freezed: ^2.4.2
|
||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
|
|||||||
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -624,6 +624,7 @@ void CliprdrStream_Delete(CliprdrStream *instance)
|
|||||||
if (instance)
|
if (instance)
|
||||||
{
|
{
|
||||||
free(instance->iStream.lpVtbl);
|
free(instance->iStream.lpVtbl);
|
||||||
|
instance->iStream.lpVtbl = NULL;
|
||||||
free(instance);
|
free(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2160,7 +2161,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
/* add to name array */
|
/* 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])
|
if (!clipboard->file_names[clipboard->nFiles])
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
||||||
|
|
||||||
use hbb_common::libc::c_int;
|
use hbb_common::libc::c_int;
|
||||||
|
use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay};
|
||||||
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
|
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
|
||||||
use std::{borrow::Cow, ffi::CString};
|
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
|
/// The main struct for handling the event emitting
|
||||||
pub(super) struct EnigoXdo {
|
pub(super) struct EnigoXdo {
|
||||||
xdo: *mut xdo_t,
|
xdo: *mut xdo_t,
|
||||||
@@ -52,6 +98,7 @@ impl Default for EnigoXdo {
|
|||||||
log::warn!("Failed to create xdo context, xdo functions will be disabled");
|
log::warn!("Failed to create xdo context, xdo functions will be disabled");
|
||||||
} else {
|
} else {
|
||||||
log::info!("xdo context created successfully");
|
log::info!("xdo context created successfully");
|
||||||
|
check_x11_button_map();
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
xdo,
|
xdo,
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ impl Session {
|
|||||||
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
||||||
let mut password = "".to_owned();
|
let mut password = "".to_owned();
|
||||||
if PeerConfig::load(id).password.is_empty() {
|
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 {
|
let session = Self {
|
||||||
id: id.to_owned(),
|
id: id.to_owned(),
|
||||||
|
|||||||
@@ -1745,6 +1745,9 @@ pub struct LoginConfigHandler {
|
|||||||
pub direct: Option<bool>,
|
pub direct: Option<bool>,
|
||||||
pub received: bool,
|
pub received: bool,
|
||||||
switch_uuid: Option<String>,
|
switch_uuid: Option<String>,
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
switch_back_allowed: bool,
|
||||||
pub save_ab_password_to_recent: bool, // true: connected with ab password
|
pub save_ab_password_to_recent: bool, // true: connected with ab password
|
||||||
pub other_server: Option<(String, String, String)>,
|
pub other_server: Option<(String, String, String)>,
|
||||||
pub custom_fps: Arc<Mutex<Option<usize>>>,
|
pub custom_fps: Arc<Mutex<Option<usize>>>,
|
||||||
@@ -1861,6 +1864,11 @@ impl LoginConfigHandler {
|
|||||||
|
|
||||||
self.direct = None;
|
self.direct = None;
|
||||||
self.received = false;
|
self.received = false;
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
{
|
||||||
|
self.switch_back_allowed = false;
|
||||||
|
}
|
||||||
self.switch_uuid = switch_uuid;
|
self.switch_uuid = switch_uuid;
|
||||||
self.adapter_luid = adapter_luid;
|
self.adapter_luid = adapter_luid;
|
||||||
self.selected_windows_session_id = None;
|
self.selected_windows_session_id = None;
|
||||||
@@ -1874,6 +1882,23 @@ impl LoginConfigHandler {
|
|||||||
self.is_terminal_admin = is_terminal_admin;
|
self.is_terminal_admin = is_terminal_admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn allow_switch_back_once(&mut self) {
|
||||||
|
self.switch_back_allowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn consume_switch_back_permission(&mut self) -> bool {
|
||||||
|
if self.switch_back_allowed {
|
||||||
|
self.switch_back_allowed = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if the client should auto login.
|
/// Check if the client should auto login.
|
||||||
/// Return password if the client should auto login, otherwise return empty string.
|
/// Return password if the client should auto login, otherwise return empty string.
|
||||||
pub fn should_auto_login(&self) -> String {
|
pub fn should_auto_login(&self) -> String {
|
||||||
@@ -3377,6 +3402,36 @@ pub fn handle_login_error(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
|
||||||
|
let Ok(mut conn) = crate::ipc::connect(1000, "").await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let uuid = uuid.to_string();
|
||||||
|
if conn
|
||||||
|
.send(&crate::ipc::Data::SwitchSidesUuid(
|
||||||
|
uuid.clone(),
|
||||||
|
id.to_owned(),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match conn.next_timeout(1000).await {
|
||||||
|
Ok(Some(crate::ipc::Data::SwitchSidesUuid(
|
||||||
|
returned_uuid,
|
||||||
|
returned_id,
|
||||||
|
Some(true),
|
||||||
|
))) => {
|
||||||
|
returned_uuid == uuid && returned_id == id
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle hash message sent by peer.
|
/// Handle hash message sent by peer.
|
||||||
/// Hash will be used for login.
|
/// Hash will be used for login.
|
||||||
///
|
///
|
||||||
@@ -3397,12 +3452,22 @@ pub async fn handle_hash(
|
|||||||
// Take care of password application order
|
// Take care of password application order
|
||||||
|
|
||||||
// switch_uuid
|
// switch_uuid
|
||||||
let uuid = lc.write().unwrap().switch_uuid.take();
|
#[cfg(feature = "flutter")]
|
||||||
if let Some(uuid) = uuid {
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
{
|
||||||
send_switch_login_request(lc.clone(), peer, uuid).await;
|
let uuid = lc.write().unwrap().switch_uuid.take();
|
||||||
lc.write().unwrap().password_source = Default::default();
|
if let Some(uuid) = uuid {
|
||||||
return;
|
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
||||||
|
let id = lc.read().unwrap().id.clone();
|
||||||
|
if !consume_local_switch_sides_uuid(&id, &uuid).await {
|
||||||
|
log::warn!("Ignored untrusted switch_uuid");
|
||||||
|
} else {
|
||||||
|
lc.write().unwrap().allow_switch_back_once();
|
||||||
|
send_switch_login_request(lc.clone(), peer, uuid).await;
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// last password
|
// last password
|
||||||
|
|||||||
@@ -1448,6 +1448,23 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
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")]
|
#[cfg(target_os = "android")]
|
||||||
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
||||||
}
|
}
|
||||||
@@ -1780,6 +1797,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
Ok(Permission::BlockInput) => {
|
Ok(Permission::BlockInput) => {
|
||||||
self.handler.set_permission("block_input", p.enabled);
|
self.handler.set_permission("block_input", p.enabled);
|
||||||
}
|
}
|
||||||
|
Ok(Permission::PrivacyMode) => {
|
||||||
|
self.handler.set_permission("privacy_mode", p.enabled);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1903,9 +1923,23 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Some(misc::Union::SwitchBack(_)) => {
|
Some(misc::Union::SwitchBack(_)) => {
|
||||||
#[cfg(feature = "flutter")]
|
let allow_switch_back = self
|
||||||
self.handler.switch_back(&self.handler.get_id());
|
.handler
|
||||||
|
.lc
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.consume_switch_back_permission();
|
||||||
|
if allow_switch_back {
|
||||||
|
self.handler.switch_back(&self.handler.get_id());
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Ignored unsolicited SwitchBack from {}",
|
||||||
|
self.handler.get_id()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
|||||||
@@ -605,21 +605,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.
|
// 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.
|
// session_enter_or_leave() will be called then.
|
||||||
// As rust is multi-thread, it is possible that enter() is called before leave().
|
// As Rust is multi-threaded, enter() can be called before leave().
|
||||||
// This will cause the keyboard input to take no effect.
|
// The Rust-side grab ownership state filters stale transitions.
|
||||||
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
|
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
|
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
|
||||||
let keyboard_mode = session.get_keyboard_mode();
|
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 {
|
if _enter {
|
||||||
set_cur_session_id_(_session_id, &keyboard_mode);
|
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 {
|
} else {
|
||||||
session.leave(keyboard_mode);
|
crate::keyboard::client::change_grab_status(
|
||||||
|
crate::common::GrabState::Wait,
|
||||||
|
&keyboard_mode,
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyncReturn(())
|
SyncReturn(())
|
||||||
@@ -963,6 +972,27 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_option(key: String, value: String) {
|
pub fn main_set_option(key: String, value: String) {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD)
|
||||||
|
|| key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER)
|
||||||
|
|| key.eq(config::keys::OPTION_ENABLE_AUDIO);
|
||||||
|
let allow_perm_change_in_accept_window = config::option2bool(
|
||||||
|
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
);
|
||||||
|
if is_permission_option
|
||||||
|
&& !allow_perm_change_in_accept_window
|
||||||
|
&& crate::ui_cm_interface::has_active_clients()
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"blocked main_set_option by policy, key={}, value={}",
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
|
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
|
||||||
crate::ui_cm_interface::switch_permission_all(
|
crate::ui_cm_interface::switch_permission_all(
|
||||||
@@ -1010,7 +1040,29 @@ pub fn main_get_options_sync() -> SyncReturn<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_options(json: String) {
|
pub fn main_set_options(json: String) {
|
||||||
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
|
let mut map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let allow_perm_change_in_accept_window = config::option2bool(
|
||||||
|
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
);
|
||||||
|
if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() {
|
||||||
|
for key in [
|
||||||
|
config::keys::OPTION_ENABLE_CLIPBOARD,
|
||||||
|
config::keys::OPTION_ENABLE_FILE_TRANSFER,
|
||||||
|
config::keys::OPTION_ENABLE_AUDIO,
|
||||||
|
] {
|
||||||
|
if let Some(value) = map.remove(key) {
|
||||||
|
log::info!(
|
||||||
|
"blocked main_set_options item by policy, key={}, value={}",
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if !map.is_empty() {
|
if !map.is_empty() {
|
||||||
set_options(map)
|
set_options(map)
|
||||||
}
|
}
|
||||||
@@ -2161,7 +2213,7 @@ pub fn cm_elevate_portable(conn_id: i32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_switch_back(conn_id: i32) {
|
pub fn cm_switch_back(conn_id: i32) {
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
crate::ui_cm_interface::switch_back(conn_id);
|
crate::ui_cm_interface::switch_back(conn_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
src/ipc.rs
23
src/ipc.rs
@@ -237,6 +237,7 @@ pub enum Data {
|
|||||||
restart: bool,
|
restart: bool,
|
||||||
recording: bool,
|
recording: bool,
|
||||||
block_input: bool,
|
block_input: bool,
|
||||||
|
privacy_mode: bool,
|
||||||
from_switch: bool,
|
from_switch: bool,
|
||||||
},
|
},
|
||||||
ChatMessage {
|
ChatMessage {
|
||||||
@@ -284,7 +285,14 @@ pub enum Data {
|
|||||||
Empty,
|
Empty,
|
||||||
Disconnected,
|
Disconnected,
|
||||||
DataPortableService(DataPortableService),
|
DataPortableService(DataPortableService),
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
SwitchSidesRequest(String),
|
SwitchSidesRequest(String),
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
SwitchSidesUuid(String, String, Option<bool>),
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
SwitchSidesBack,
|
SwitchSidesBack,
|
||||||
UrlLink(String),
|
UrlLink(String),
|
||||||
VoiceCallIncoming,
|
VoiceCallIncoming,
|
||||||
@@ -770,6 +778,8 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
Data::TestRendezvousServer => {
|
Data::TestRendezvousServer => {
|
||||||
crate::test_rendezvous_server();
|
crate::test_rendezvous_server();
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Data::SwitchSidesRequest(id) => {
|
Data::SwitchSidesRequest(id) => {
|
||||||
let uuid = uuid::Uuid::new_v4();
|
let uuid = uuid::Uuid::new_v4();
|
||||||
crate::server::insert_switch_sides_uuid(id, uuid.clone());
|
crate::server::insert_switch_sides_uuid(id, uuid.clone());
|
||||||
@@ -779,6 +789,19 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
.await
|
.await
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
Data::SwitchSidesUuid(uuid, id, None) => {
|
||||||
|
let allowed = uuid
|
||||||
|
.parse::<uuid::Uuid>()
|
||||||
|
.map(|uuid| crate::server::remove_pending_switch_sides_uuid(&id, &uuid))
|
||||||
|
.unwrap_or(false);
|
||||||
|
allow_err!(
|
||||||
|
stream
|
||||||
|
.send(&Data::SwitchSidesUuid(uuid, id, Some(allowed)))
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
}
|
||||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await,
|
Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await,
|
||||||
|
|||||||
223
src/keyboard.rs
223
src/keyboard.rs
@@ -82,8 +82,67 @@ lazy_static::lazy_static! {
|
|||||||
pub mod client {
|
pub mod client {
|
||||||
use super::*;
|
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! {
|
lazy_static::lazy_static! {
|
||||||
static ref IS_GRAB_STARTED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
|
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() {
|
pub fn start_grab_loop() {
|
||||||
@@ -96,36 +155,167 @@ pub mod client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[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")]
|
#[cfg(feature = "flutter")]
|
||||||
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
|
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
|
||||||
return;
|
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 {
|
match state {
|
||||||
GrabState::Ready => {}
|
GrabState::Ready => {}
|
||||||
GrabState::Run => {
|
GrabState::Run => {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
update_grab_get_key_name(keyboard_mode);
|
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"))]
|
#[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")]
|
#[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 => {
|
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)]
|
#[cfg(windows)]
|
||||||
rdev::set_get_key_unicode(false);
|
rdev::set_get_key_unicode(false);
|
||||||
|
|
||||||
release_remote_keys(keyboard_mode);
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[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")]
|
#[cfg(target_os = "linux")]
|
||||||
rdev::disable_grab();
|
{
|
||||||
|
disable_after_unlock = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GrabState::Exit => {}
|
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>) {
|
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
|
||||||
@@ -341,7 +531,6 @@ fn notify_exit_relative_mouse_mode() {
|
|||||||
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
|
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handle relative mouse mode shortcuts in the rdev grab loop.
|
/// Handle relative mouse mode shortcuts in the rdev grab loop.
|
||||||
/// Returns true if the event should be blocked from being sent to the peer.
|
/// Returns true if the event should be blocked from being sent to the peer.
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
@@ -540,10 +729,12 @@ pub fn is_long_press(event: &Event) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn release_remote_keys(keyboard_mode: &str) {
|
fn take_remote_keys() -> HashMap<Key, Event> {
|
||||||
// todo!: client quit suddenly, how to release keys?
|
let mut to_release = TO_RELEASE.lock().unwrap();
|
||||||
let to_release = TO_RELEASE.lock().unwrap().clone();
|
std::mem::take(&mut *to_release)
|
||||||
TO_RELEASE.lock().unwrap().clear();
|
}
|
||||||
|
|
||||||
|
fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap<Key, Event>) {
|
||||||
for (key, mut event) in to_release.into_iter() {
|
for (key, mut event) in to_release.into_iter() {
|
||||||
event.event_type = EventType::KeyRelease(key);
|
event.event_type = EventType::KeyRelease(key);
|
||||||
client::process_event(keyboard_mode, &event, None);
|
client::process_event(keyboard_mode, &event, None);
|
||||||
@@ -558,6 +749,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 {
|
pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode {
|
||||||
match keyboard_mode {
|
match keyboard_mode {
|
||||||
"map" => KeyboardMode::Map,
|
"map" => KeyboardMode::Map,
|
||||||
@@ -748,7 +945,6 @@ pub fn event_to_key_events(
|
|||||||
) -> Vec<KeyEvent> {
|
) -> Vec<KeyEvent> {
|
||||||
peer.retain(|c| !c.is_whitespace());
|
peer.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
let mut key_event = KeyEvent::new();
|
|
||||||
update_modifiers_state(event);
|
update_modifiers_state(event);
|
||||||
|
|
||||||
match event.event_type {
|
match event.event_type {
|
||||||
@@ -761,6 +957,7 @@ pub fn event_to_key_events(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut key_event = KeyEvent::new();
|
||||||
key_event.mode = keyboard_mode.into();
|
key_event.mode = keyboard_mode.into();
|
||||||
|
|
||||||
let mut key_events = match keyboard_mode {
|
let mut key_events = match keyboard_mode {
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "اسم العرض"),
|
("Display Name", "اسم العرض"),
|
||||||
("password-hidden-tip", "كلمة المرور مخفية"),
|
("password-hidden-tip", "كلمة المرور مخفية"),
|
||||||
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
|
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Імя для адлюстравання"),
|
("Display Name", "Імя для адлюстравання"),
|
||||||
("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."),
|
("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."),
|
||||||
("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"),
|
("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "显示名称"),
|
("Display Name", "显示名称"),
|
||||||
("password-hidden-tip", "永久密码已设置(已隐藏)"),
|
("password-hidden-tip", "永久密码已设置(已隐藏)"),
|
||||||
("preset-password-in-use-tip", "当前使用预设密码"),
|
("preset-password-in-use-tip", "当前使用预设密码"),
|
||||||
|
("Enable privacy mode", "允许隐私模式"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Anzeigename"),
|
("Display Name", "Anzeigename"),
|
||||||
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
|
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
|
||||||
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
|
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
|
||||||
|
("Enable privacy mode", "Datenschutzmodus aktivieren"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Εμφανιζόμενο όνομα"),
|
("Display Name", "Εμφανιζόμενο όνομα"),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -741,7 +741,8 @@ 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"),
|
("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"),
|
||||||
("Continue with {}", "Continuer avec {}"),
|
("Continue with {}", "Continuer avec {}"),
|
||||||
("Display Name", "Nom d’affichage"),
|
("Display Name", "Nom d’affichage"),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."),
|
||||||
|
("Enable privacy mode", "Activer le mode de confidentialité"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -742,5 +742,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "ડિસ્પ્લે નામ"),
|
("Display Name", "ડિસ્પ્લે નામ"),
|
||||||
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
|
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
|
||||||
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
|
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Kijelző név"),
|
("Display Name", "Kijelző név"),
|
||||||
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
|
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
|
||||||
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
|
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Visualizza nome"),
|
("Display Name", "Visualizza nome"),
|
||||||
("password-hidden-tip", "È impostata una password permanente (nascosta)."),
|
("password-hidden-tip", "È impostata una password permanente (nascosta)."),
|
||||||
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
|
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
|
||||||
|
("Enable privacy mode", "Abilita modalità privacy"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "表示名"),
|
("Display Name", "表示名"),
|
||||||
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
|
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
|
||||||
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
|
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "표시 이름"),
|
("Display Name", "표시 이름"),
|
||||||
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
|
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
|
||||||
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
|
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Naam Weergeven"),
|
("Display Name", "Naam Weergeven"),
|
||||||
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
|
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
|
||||||
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
|
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Nazwa wyświetlana"),
|
("Display Name", "Nazwa wyświetlana"),
|
||||||
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
|
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
|
||||||
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
|
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Nume afișat"),
|
("Display Name", "Nume afișat"),
|
||||||
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
|
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
|
||||||
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
|
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "Отображаемое имя"),
|
("Display Name", "Отображаемое имя"),
|
||||||
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
|
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
|
||||||
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
|
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
|
||||||
|
("Enable privacy mode", "Использовать режим конфиденциальности"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -741,7 +741,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"),
|
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"),
|
||||||
("Continue with {}", "{} ile devam et"),
|
("Continue with {}", "{} ile devam et"),
|
||||||
("Display Name", "Görünen Ad"),
|
("Display Name", "Görünen Ad"),
|
||||||
("password-hidden-tip", "Şifre gizli"),
|
("password-hidden-tip", "Parola gizli"),
|
||||||
("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"),
|
("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"),
|
||||||
|
("Enable privacy mode", "Gizlilik modunu etkinleştir"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", "顯示名稱"),
|
("Display Name", "顯示名稱"),
|
||||||
("password-hidden-tip", "固定密碼已設定(已隱藏)"),
|
("password-hidden-tip", "固定密碼已設定(已隱藏)"),
|
||||||
("preset-password-in-use-tip", "目前正在使用預設密碼"),
|
("preset-password-in-use-tip", "目前正在使用預設密碼"),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Display Name", ""),
|
("Display Name", ""),
|
||||||
("password-hidden-tip", ""),
|
("password-hidden-tip", ""),
|
||||||
("preset-password-in-use-tip", ""),
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use hbb_common::{
|
|||||||
anyhow::anyhow,
|
anyhow::anyhow,
|
||||||
bail,
|
bail,
|
||||||
config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config},
|
config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config},
|
||||||
libc::{c_char, c_int, c_long, c_uint, c_void},
|
libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void},
|
||||||
log,
|
log,
|
||||||
message_proto::{DisplayInfo, Resolution},
|
message_proto::{DisplayInfo, Resolution},
|
||||||
regex::{Captures, Regex},
|
regex::{Captures, Regex},
|
||||||
@@ -97,10 +97,55 @@ thread_local! {
|
|||||||
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
|
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X11 error event structure for the custom error handler.
|
||||||
|
// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers
|
||||||
|
#[repr(C)]
|
||||||
|
struct XErrorEvent {
|
||||||
|
type_: c_int,
|
||||||
|
display: *mut c_void, // Display*
|
||||||
|
resourceid: c_ulong, // XID
|
||||||
|
serial: c_ulong,
|
||||||
|
error_code: u8,
|
||||||
|
request_code: u8,
|
||||||
|
minor_code: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int;
|
||||||
|
|
||||||
|
const X11_BAD_WINDOW: u8 = 3;
|
||||||
|
const XDO_SUCCESS: c_int = 0;
|
||||||
|
const XDO_ERROR: c_int = 1;
|
||||||
|
|
||||||
|
/// Atomic flag set by the custom X error handler when a BadWindow error occurs.
|
||||||
|
static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of
|
||||||
|
/// letting the default handler terminate the process.
|
||||||
|
/// See issue: https://github.com/rustdesk/rustdesk/issues/9003
|
||||||
|
unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int {
|
||||||
|
if !event.is_null() && (*event).error_code == X11_BAD_WINDOW {
|
||||||
|
X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst);
|
||||||
|
log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst);
|
||||||
|
if !event.is_null() {
|
||||||
|
log::warn!(
|
||||||
|
"X11 error: error_code={}, request_code={}, minor_code={}",
|
||||||
|
(*event).error_code,
|
||||||
|
(*event).request_code,
|
||||||
|
(*event).minor_code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
#[link(name = "X11")]
|
#[link(name = "X11")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
|
fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
|
||||||
// fn XCloseDisplay(d: *mut c_void) -> c_int;
|
// fn XCloseDisplay(d: *mut c_void) -> c_int;
|
||||||
|
fn XSetErrorHandler(handler: Option<XErrorHandler>) -> Option<XErrorHandler>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[link(name = "Xfixes")]
|
#[link(name = "Xfixes")]
|
||||||
@@ -231,25 +276,47 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
|||||||
if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 {
|
if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if libxdo_sys::xdo_get_window_location(
|
|
||||||
|
// XSetErrorHandler is process-global, not scoped to this Display/thread.
|
||||||
|
// This path is currently called by the single window_focus service thread.
|
||||||
|
// While installed, this handler can still observe unrelated X11 errors from
|
||||||
|
// other threads; unexpected errors make this geometry query fail.
|
||||||
|
X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst);
|
||||||
|
X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst);
|
||||||
|
let prev_handler = XSetErrorHandler(Some(handle_x_error));
|
||||||
|
|
||||||
|
let loc_ret = libxdo_sys::xdo_get_window_location(
|
||||||
*xdo as *const _,
|
*xdo as *const _,
|
||||||
window,
|
window,
|
||||||
&mut x as _,
|
&mut x as _,
|
||||||
&mut y as _,
|
&mut y as _,
|
||||||
std::ptr::null_mut(),
|
std::ptr::null_mut(),
|
||||||
) != 0
|
);
|
||||||
{
|
let size_ret = if loc_ret == XDO_SUCCESS {
|
||||||
return;
|
libxdo_sys::xdo_get_window_size(
|
||||||
}
|
*xdo as *const _,
|
||||||
if libxdo_sys::xdo_get_window_size(
|
window,
|
||||||
*xdo as *const _,
|
&mut width,
|
||||||
window,
|
&mut height,
|
||||||
&mut width,
|
)
|
||||||
&mut height,
|
} else {
|
||||||
) != 0
|
XDO_ERROR
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do not call XSync(DISPLAY) here: DISPLAY is a separate
|
||||||
|
// XOpenDisplay() connection, while libxdo owns the Display*
|
||||||
|
// used by these geometry queries. These libxdo calls are
|
||||||
|
// synchronous XGetWindowAttributes-based queries, so the target
|
||||||
|
// BadWindow is expected to be delivered before the calls return.
|
||||||
|
XSetErrorHandler(prev_handler);
|
||||||
|
if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst)
|
||||||
|
|| X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst)
|
||||||
|
|| loc_ret != XDO_SUCCESS
|
||||||
|
|| size_ret != XDO_SUCCESS
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let center_x = x + (width / 2) as c_int;
|
let center_x = x + (width / 2) as c_int;
|
||||||
let center_y = y + (height / 2) as c_int;
|
let center_y = y + (height / 2) as c_int;
|
||||||
res = displays.iter().position(|d| {
|
res = displays.iter().position(|d| {
|
||||||
@@ -2150,7 +2217,10 @@ pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> {
|
|||||||
|| err_name == "org.freedesktop.DBus.Error.UnknownObject"
|
|| err_name == "org.freedesktop.DBus.Error.UnknownObject"
|
||||||
|| err_name == "org.freedesktop.DBus.Error.ServiceUnknown"
|
|| err_name == "org.freedesktop.DBus.Error.ServiceUnknown"
|
||||||
{
|
{
|
||||||
log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name);
|
log::info!(
|
||||||
|
"GNOME shortcuts inhibitor permission was not set ({})",
|
||||||
|
err_name
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
bail!("Failed to clear permission: {}", e)
|
bail!("Failed to clear permission: {}", e)
|
||||||
|
|||||||
@@ -73,11 +73,17 @@ lazy_static::lazy_static! {
|
|||||||
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
|
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
|
||||||
pub static ref AUTHED_CONNS: Arc::<Mutex<Vec<AuthedConn>>> = Default::default();
|
pub static ref AUTHED_CONNS: Arc::<Mutex<Vec<AuthedConn>>> = Default::default();
|
||||||
pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::<Mutex<Vec<(i32, ControlPermissions)>>> = Default::default();
|
pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::<Mutex<Vec<(i32, ControlPermissions)>>> = Default::default();
|
||||||
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
|
||||||
static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread()));
|
static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread()));
|
||||||
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
||||||
|
static ref PENDING_SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||||
if a.len() != b.len() {
|
if a.len() != b.len() {
|
||||||
return false;
|
return false;
|
||||||
@@ -241,6 +247,7 @@ pub struct Connection {
|
|||||||
restart: bool,
|
restart: bool,
|
||||||
recording: bool,
|
recording: bool,
|
||||||
block_input: bool,
|
block_input: bool,
|
||||||
|
privacy_mode: bool,
|
||||||
control_permissions: Option<ControlPermissions>,
|
control_permissions: Option<ControlPermissions>,
|
||||||
last_test_delay: Option<Instant>,
|
last_test_delay: Option<Instant>,
|
||||||
network_delay: u32,
|
network_delay: u32,
|
||||||
@@ -431,6 +438,7 @@ impl Connection {
|
|||||||
restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions),
|
restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions),
|
||||||
recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions),
|
recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions),
|
||||||
block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions),
|
block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions),
|
||||||
|
privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions),
|
||||||
control_permissions,
|
control_permissions,
|
||||||
last_test_delay: None,
|
last_test_delay: None,
|
||||||
network_delay: 0,
|
network_delay: 0,
|
||||||
@@ -527,6 +535,9 @@ impl Connection {
|
|||||||
if !conn.block_input {
|
if !conn.block_input {
|
||||||
conn.send_permission(Permission::BlockInput, false).await;
|
conn.send_permission(Permission::BlockInput, false).await;
|
||||||
}
|
}
|
||||||
|
if !conn.privacy_mode {
|
||||||
|
conn.send_permission(Permission::PrivacyMode, false).await;
|
||||||
|
}
|
||||||
let mut test_delay_timer =
|
let mut test_delay_timer =
|
||||||
crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT));
|
crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT));
|
||||||
let mut last_recv_time = Instant::now();
|
let mut last_recv_time = Instant::now();
|
||||||
@@ -674,6 +685,46 @@ impl Connection {
|
|||||||
} else if &name == "block_input" {
|
} else if &name == "block_input" {
|
||||||
conn.block_input = enabled;
|
conn.block_input = enabled;
|
||||||
conn.send_permission(Permission::BlockInput, enabled).await;
|
conn.send_permission(Permission::BlockInput, enabled).await;
|
||||||
|
} else if &name == "privacy_mode" {
|
||||||
|
// Keep permission state and runtime state consistent:
|
||||||
|
// when revoking the permission, try to leave privacy mode first.
|
||||||
|
// Otherwise we could end up in an inconsistent state where
|
||||||
|
// permission looks disabled while privacy mode is still active.
|
||||||
|
if !enabled && privacy_mode::is_in_privacy_mode() {
|
||||||
|
if let Some(conn_id) = privacy_mode::get_privacy_mode_conn_id() {
|
||||||
|
if conn_id == conn.inner.id() {
|
||||||
|
let impl_key =
|
||||||
|
privacy_mode::get_cur_impl_key().unwrap_or_default();
|
||||||
|
let turn_off_res =
|
||||||
|
privacy_mode::turn_off_privacy(conn_id, None);
|
||||||
|
match turn_off_res {
|
||||||
|
Some(Ok(_)) => {
|
||||||
|
let msg_out = crate::common::make_privacy_mode_msg(
|
||||||
|
back_notification::PrivacyModeState::PrvOffByPeer,
|
||||||
|
impl_key.clone(),
|
||||||
|
);
|
||||||
|
conn.send(msg_out).await;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let msg_out = Self::turn_off_privacy_result_to_msg(
|
||||||
|
turn_off_res,
|
||||||
|
impl_key,
|
||||||
|
);
|
||||||
|
conn.send(msg_out).await;
|
||||||
|
// Turn-off failed, so revert CM's optimistic toggle
|
||||||
|
// and keep the previous permission value.
|
||||||
|
conn.send_to_cm(ipc::Data::SwitchPermission {
|
||||||
|
name: "privacy_mode".to_owned(),
|
||||||
|
enabled: conn.privacy_mode,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.privacy_mode = enabled;
|
||||||
|
conn.send_permission(Permission::PrivacyMode, enabled).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ipc::Data::RawMessage(bytes) => {
|
ipc::Data::RawMessage(bytes) => {
|
||||||
@@ -730,6 +781,8 @@ impl Connection {
|
|||||||
log::error!("Failed to start portable service from cm: {:?}", e);
|
log::error!("Failed to start portable service from cm: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
ipc::Data::SwitchSidesBack => {
|
ipc::Data::SwitchSidesBack => {
|
||||||
let mut misc = Misc::new();
|
let mut misc = Misc::new();
|
||||||
misc.set_switch_back(SwitchBack::default());
|
misc.set_switch_back(SwitchBack::default());
|
||||||
@@ -978,7 +1031,7 @@ impl Connection {
|
|||||||
|
|
||||||
if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() {
|
if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() {
|
||||||
if video_privacy_conn_id == id {
|
if video_privacy_conn_id == id {
|
||||||
let _ = Self::turn_off_privacy_to_msg(id);
|
let _ = Self::turn_off_privacy_to_msg(id, String::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||||
@@ -1900,6 +1953,7 @@ impl Connection {
|
|||||||
restart: self.restart,
|
restart: self.restart,
|
||||||
recording: self.recording,
|
recording: self.recording,
|
||||||
block_input: self.block_input,
|
block_input: self.block_input,
|
||||||
|
privacy_mode: self.privacy_mode,
|
||||||
from_switch: self.from_switch,
|
from_switch: self.from_switch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2175,6 +2229,7 @@ impl Connection {
|
|||||||
keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart),
|
keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart),
|
||||||
keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording),
|
keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording),
|
||||||
keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input),
|
keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input),
|
||||||
|
keys::OPTION_ENABLE_PRIVACY_MODE => Some(Permission::privacy_mode),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(permission) = permission {
|
if let Some(permission) = permission {
|
||||||
@@ -2532,6 +2587,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
} else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union {
|
} else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union {
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(lr) = _s.lr.clone().take() {
|
if let Some(lr) = _s.lr.clone().take() {
|
||||||
self.handle_login_request_without_validation(&lr).await;
|
self.handle_login_request_without_validation(&lr).await;
|
||||||
SWITCH_SIDES_UUID
|
SWITCH_SIDES_UUID
|
||||||
@@ -3247,8 +3303,13 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Some(misc::Union::SwitchSidesRequest(s)) => {
|
Some(misc::Union::SwitchSidesRequest(s)) => {
|
||||||
if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) {
|
if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) {
|
||||||
|
crate::server::insert_pending_switch_sides_uuid(
|
||||||
|
self.lr.my_id.clone(),
|
||||||
|
uuid.clone(),
|
||||||
|
);
|
||||||
crate::run_me(vec![
|
crate::run_me(vec![
|
||||||
"--connect",
|
"--connect",
|
||||||
&self.lr.my_id,
|
&self.lr.my_id,
|
||||||
@@ -4145,6 +4206,15 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn turn_on_privacy(&mut self, impl_key: String) {
|
async fn turn_on_privacy(&mut self, impl_key: String) {
|
||||||
|
if !self.is_authed_remote_conn() || !self.privacy_mode {
|
||||||
|
let msg_out = crate::common::make_privacy_mode_msg(
|
||||||
|
back_notification::PrivacyModeState::PrvOnFailedDenied,
|
||||||
|
impl_key,
|
||||||
|
);
|
||||||
|
self.send(msg_out).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let msg_out = if !privacy_mode::is_privacy_mode_supported() {
|
let msg_out = if !privacy_mode::is_privacy_mode_supported() {
|
||||||
crate::common::make_privacy_mode_msg_with_details(
|
crate::common::make_privacy_mode_msg_with_details(
|
||||||
back_notification::PrivacyModeState::PrvNotSupported,
|
back_notification::PrivacyModeState::PrvNotSupported,
|
||||||
@@ -4186,7 +4256,7 @@ impl Connection {
|
|||||||
"Check privacy mode failed: {}, turn off privacy mode.",
|
"Check privacy mode failed: {}, turn off privacy mode.",
|
||||||
&err_msg
|
&err_msg
|
||||||
);
|
);
|
||||||
let _ = Self::turn_off_privacy_to_msg(self.inner.id);
|
let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new());
|
||||||
crate::common::make_privacy_mode_msg_with_details(
|
crate::common::make_privacy_mode_msg_with_details(
|
||||||
back_notification::PrivacyModeState::PrvOnFailed,
|
back_notification::PrivacyModeState::PrvOnFailed,
|
||||||
err_msg,
|
err_msg,
|
||||||
@@ -4205,6 +4275,7 @@ impl Connection {
|
|||||||
if privacy_mode::is_in_privacy_mode() {
|
if privacy_mode::is_in_privacy_mode() {
|
||||||
let _ = Self::turn_off_privacy_to_msg(
|
let _ = Self::turn_off_privacy_to_msg(
|
||||||
privacy_mode::INVALID_PRIVACY_MODE_CONN_ID,
|
privacy_mode::INVALID_PRIVACY_MODE_CONN_ID,
|
||||||
|
String::new(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
crate::common::make_privacy_mode_msg_with_details(
|
crate::common::make_privacy_mode_msg_with_details(
|
||||||
@@ -4232,14 +4303,23 @@ impl Connection {
|
|||||||
impl_key,
|
impl_key,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Self::turn_off_privacy_to_msg(self.inner.id)
|
Self::turn_off_privacy_to_msg(self.inner.id, impl_key)
|
||||||
};
|
};
|
||||||
self.send(msg_out).await;
|
self.send(msg_out).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn turn_off_privacy_to_msg(_conn_id: i32) -> Message {
|
pub fn turn_off_privacy_to_msg(_conn_id: i32, impl_key: String) -> Message {
|
||||||
let impl_key = "".to_owned();
|
Self::turn_off_privacy_result_to_msg(
|
||||||
match privacy_mode::turn_off_privacy(_conn_id, None) {
|
privacy_mode::turn_off_privacy(_conn_id, None),
|
||||||
|
impl_key,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_off_privacy_result_to_msg(
|
||||||
|
turn_off_res: Option<hbb_common::ResultType<()>>,
|
||||||
|
impl_key: String,
|
||||||
|
) -> Message {
|
||||||
|
match turn_off_res {
|
||||||
Some(Ok(_)) => crate::common::make_privacy_mode_msg(
|
Some(Ok(_)) => crate::common::make_privacy_mode_msg(
|
||||||
back_notification::PrivacyModeState::PrvOffSucceeded,
|
back_notification::PrivacyModeState::PrvOffSucceeded,
|
||||||
impl_key,
|
impl_key,
|
||||||
@@ -4872,6 +4952,8 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||||
SWITCH_SIDES_UUID
|
SWITCH_SIDES_UUID
|
||||||
.lock()
|
.lock()
|
||||||
@@ -4879,6 +4961,27 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
|||||||
.insert(id, (tokio::time::Instant::now(), uuid));
|
.insert(id, (tokio::time::Instant::now(), uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn insert_pending_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||||
|
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
||||||
|
uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10));
|
||||||
|
uuids.insert(id, (tokio::time::Instant::now(), uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
|
||||||
|
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
||||||
|
uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10));
|
||||||
|
if uuids.get(id).map(|(_, stored_uuid)| stored_uuid == uuid) == Some(true) {
|
||||||
|
uuids.remove(id);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
async fn start_ipc(
|
async fn start_ipc(
|
||||||
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
|
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
|
||||||
|
|||||||
@@ -372,6 +372,11 @@ impl UI {
|
|||||||
is_installed()
|
is_installed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_supported_privacy_mode_impls(&self) -> String {
|
||||||
|
serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
fn is_root(&self) -> bool {
|
fn is_root(&self) -> bool {
|
||||||
is_root()
|
is_root()
|
||||||
}
|
}
|
||||||
@@ -752,6 +757,7 @@ impl sciter::EventHandler for UI {
|
|||||||
fn get_icon();
|
fn get_icon();
|
||||||
fn install_me(String, String);
|
fn install_me(String, String);
|
||||||
fn is_installed();
|
fn is_installed();
|
||||||
|
fn get_supported_privacy_mode_impls();
|
||||||
fn is_root();
|
fn is_root();
|
||||||
fn is_release();
|
fn is_release();
|
||||||
fn set_socks(String, String, String);
|
fn set_socks(String, String, String);
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ div.permissions > div:active {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.permissions.locked,
|
||||||
|
div.permissions.locked *,
|
||||||
|
div.permissions.locked > div:active {
|
||||||
|
cursor: default !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
icon.keyboard {
|
icon.keyboard {
|
||||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=');
|
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=');
|
||||||
}
|
}
|
||||||
@@ -121,6 +128,10 @@ icon.block_input {
|
|||||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg==');
|
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg==');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
icon.privacy_mode {
|
||||||
|
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAB7UlEQVR4AdyTrVYDMRCFuyjqiiuuOJA46sCVR6jDgQTXN+CgQIJCgkOCA0cduOLAgaOOuuW7czYhyWY5FcXQc28n85O5m9nsUuuPf/9IoCzLLnxd9MTCET3SvNckQnwL7lfcpnYueIGiKNbY8QYjERo+wZK4HuAcK94rVvGSWCO8gCqKjAixTXLPsAl7ldBxriASqAo6lfUnqUTaWAP5FajTYjxGCNXeYSRAwSflToBlKxSZKSCiMoUa6Uh+QNW/B37LC9D8lkTYHNegTf7JqNP8b5RB5AT7AkPoNqqXxUyATT28AUzhRuFFaLpDUYc9V1ihr7+EA/JdxUyAxQTWQDM3CuVSEWugGiUztJ5OIJPPhlKRbFEVXJZ1Anph8iNyTCsieA0dvIgCQY3ckBtyTIBjfuDcwRR2TPJDElkRcrpd6XcyJm7X2ATY3CKwi1UxxkNPeyiP/BAa8LVZObtdBMOPcYbvX7wXYJNE2lidBuNxyhgm0I1LCdcgFXmguXqoxhgJKELBKvYMhljH+ULEwDr8mEIRXWHSP6gJKIXIESxYh3PHzWJK1IuwjpAVcBWIhHPX0x2QE/vkHGofIzUevwr4KhZ003wvsOKYkAcxXfPoxbvk3AJuQ5MNRNwFsNKFCaibRGB0CxcqIJGU3wAAAP//8GtoDAAAAAZJREFUAwCJJuAxFVNbWwAAAABJRU5ErkJggg==');
|
||||||
|
}
|
||||||
|
|
||||||
div.outer_buttons {
|
div.outer_buttons {
|
||||||
flow:vertical;
|
flow:vertical;
|
||||||
border-spacing:8;
|
border-spacing:8;
|
||||||
|
|||||||
14
src/ui/cm.rs
14
src/ui/cm.rs
@@ -36,7 +36,8 @@ impl InvokeUiCM for SciterHandler {
|
|||||||
client.file,
|
client.file,
|
||||||
client.restart,
|
client.restart,
|
||||||
client.recording,
|
client.recording,
|
||||||
client.block_input
|
client.block_input,
|
||||||
|
client.privacy_mode
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -157,9 +158,18 @@ impl SciterConnectionManager {
|
|||||||
crate::ui_interface::get_option(key)
|
crate::ui_interface::get_option(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_builtin_option(&self, key: String) -> String {
|
||||||
|
crate::ui_interface::get_builtin_option(&key)
|
||||||
|
}
|
||||||
|
|
||||||
fn hide_cm(&self) -> bool {
|
fn hide_cm(&self) -> bool {
|
||||||
*crate::ui::cm::HIDE_CM.lock().unwrap()
|
*crate::ui::cm::HIDE_CM.lock().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_supported_privacy_mode_impls(&self) -> String {
|
||||||
|
serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sciter::EventHandler for SciterConnectionManager {
|
impl sciter::EventHandler for SciterConnectionManager {
|
||||||
@@ -181,6 +191,8 @@ impl sciter::EventHandler for SciterConnectionManager {
|
|||||||
fn can_elevate();
|
fn can_elevate();
|
||||||
fn elevate_portable(i32);
|
fn elevate_portable(i32);
|
||||||
fn get_option(String);
|
fn get_option(String);
|
||||||
|
fn get_builtin_option(String);
|
||||||
fn hide_cm();
|
fn hide_cm();
|
||||||
|
fn get_supported_privacy_mode_impls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ var body;
|
|||||||
var connections = [];
|
var connections = [];
|
||||||
var show_chat = false;
|
var show_chat = false;
|
||||||
var show_elevation = true;
|
var show_elevation = true;
|
||||||
|
var is_privacy_mode_supported = handler.get_supported_privacy_mode_impls() != '[]';
|
||||||
|
var allow_perm_change_in_accept_window =
|
||||||
|
handler.get_builtin_option('enable-perm-change-in-accept-window') != 'N';
|
||||||
var svg_elevate = <svg t="1667992597853" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1850" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M892.761 160.724v426.504c0 25.588-6.419 51.036-19.177 76.339-12.798 25.336-29.547 49.86-50.254 73.627-20.707 23.79-44.372 46.296-70.97 67.516-26.589 21.244-53.543 40.177-80.921 56.768-27.363 16.623-53.968 30.461-79.801 41.438-25.809 11.008-48.433 18.547-67.871 22.64l-9.203 1.53-8.43-1.53c-19.958-4.093-43.094-11.632-69.432-22.64-26.337-10.969-53.708-24.816-82.080-41.438-28.388-16.591-56.256-35.524-83.618-56.768-27.378-21.219-51.776-43.725-73.265-67.516-21.488-23.759-38.868-48.291-52.155-73.627-13.319-25.305-19.974-50.759-19.974-76.339v-426.504l31.455-4.629 352.892-65.97 359.784 65.97 23.017 4.629zM510.028 151.884l-4.211-0.844-302.89 51.476v269.101h307.102v-319.734zM815.434 471.634h-305.406v383.031c19.682-4.51 41.052-11.411 64.141-20.692 23.033-9.249 45.815-20.234 68.304-32.867 22.513-12.672 44.159-26.739 64.969-42.203 20.818-15.472 39.23-32.047 55.277-49.797 16.024-17.703 28.822-36.131 38.386-55.222 9.549-19.131 14.328-38.553 14.328-58.235v-124.015z" p-id="1851" fill="#ffffff"></path></svg>;
|
var svg_elevate = <svg t="1667992597853" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1850" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M892.761 160.724v426.504c0 25.588-6.419 51.036-19.177 76.339-12.798 25.336-29.547 49.86-50.254 73.627-20.707 23.79-44.372 46.296-70.97 67.516-26.589 21.244-53.543 40.177-80.921 56.768-27.363 16.623-53.968 30.461-79.801 41.438-25.809 11.008-48.433 18.547-67.871 22.64l-9.203 1.53-8.43-1.53c-19.958-4.093-43.094-11.632-69.432-22.64-26.337-10.969-53.708-24.816-82.080-41.438-28.388-16.591-56.256-35.524-83.618-56.768-27.378-21.219-51.776-43.725-73.265-67.516-21.488-23.759-38.868-48.291-52.155-73.627-13.319-25.305-19.974-50.759-19.974-76.339v-426.504l31.455-4.629 352.892-65.97 359.784 65.97 23.017 4.629zM510.028 151.884l-4.211-0.844-302.89 51.476v269.101h307.102v-319.734zM815.434 471.634h-305.406v383.031c19.682-4.51 41.052-11.411 64.141-20.692 23.033-9.249 45.815-20.234 68.304-32.867 22.513-12.672 44.159-26.739 64.969-42.203 20.818-15.472 39.23-32.047 55.277-49.797 16.024-17.703 28.822-36.131 38.386-55.222 9.549-19.131 14.328-38.553 14.328-58.235v-124.015z" p-id="1851" fill="#ffffff"></path></svg>;
|
||||||
|
|
||||||
var hide_cm = undefined;
|
var hide_cm = undefined;
|
||||||
@@ -35,6 +38,7 @@ class Body: Reactor.Component
|
|||||||
me.sendMsg(msg);
|
me.sendMsg(msg);
|
||||||
};
|
};
|
||||||
var right_style = show_chat ? "" : "display: none";
|
var right_style = show_chat ? "" : "display: none";
|
||||||
|
var permissions_locked = !allow_perm_change_in_accept_window;
|
||||||
var disconnected = c.disconnected;
|
var disconnected = c.disconnected;
|
||||||
var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0;
|
var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0;
|
||||||
var show_accept_btn = handler.get_option('approve-mode') != 'password';
|
var show_accept_btn = handler.get_option('approve-mode') != 'password';
|
||||||
@@ -58,15 +62,16 @@ class Body: Reactor.Component
|
|||||||
</div>
|
</div>
|
||||||
<div />
|
<div />
|
||||||
{c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" : <div>{translate('Permissions')}</div>}
|
{c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" : <div>{translate('Permissions')}</div>}
|
||||||
{c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" : <div> <div .permissions>
|
{c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" : <div> <div class={permissions_locked ? "permissions locked" : "permissions"} style={permissions_locked ? "opacity:0.6;" : ""}>
|
||||||
<div class={!c.keyboard ? "disabled" : ""} title={translate('Enable keyboard/mouse')}><icon .keyboard /></div>
|
<div class={!c.keyboard ? "disabled" : ""} title={translate('Enable keyboard/mouse')}><icon .keyboard /></div>
|
||||||
<div class={!c.clipboard ? "disabled" : ""} title={translate('Enable clipboard')}><icon .clipboard /></div>
|
<div class={!c.clipboard ? "disabled" : ""} title={translate('Enable clipboard')}><icon .clipboard /></div>
|
||||||
<div class={!c.audio ? "disabled" : ""} title={translate('Enable audio')}><icon .audio /></div>
|
<div class={!c.audio ? "disabled" : ""} title={translate('Enable audio')}><icon .audio /></div>
|
||||||
<div class={!c.file ? "disabled" : ""} title={translate('Enable file copy and paste')}><icon .file /></div>
|
<div class={!c.file ? "disabled" : ""} title={translate('Enable file copy and paste')}><icon .file /></div>
|
||||||
<div class={!c.restart ? "disabled" : ""} title={translate('Enable remote restart')}><icon .restart /></div>
|
<div class={!c.restart ? "disabled" : ""} title={translate('Enable remote restart')}><icon .restart /></div>
|
||||||
</div> <div .permissions style="margin-top:8px;" >
|
</div> <div class={permissions_locked ? "permissions locked" : "permissions"} style={permissions_locked ? "margin-top:8px;opacity:0.6;" : "margin-top:8px;"} >
|
||||||
<div class={!c.recording ? "disabled" : ""} title={translate('Enable recording session')}><icon .recording /></div>
|
<div class={!c.recording ? "disabled" : ""} title={translate('Enable recording session')}><icon .recording /></div>
|
||||||
<div class={!c.block_input ? "disabled" : ""} title={translate('Enable blocking user input')} style={is_win ? "" : "display:none;"}><icon .block_input /></div>
|
<div class={!c.block_input ? "disabled" : ""} title={translate('Enable blocking user input')} style={is_win ? "" : "display:none;"}><icon .block_input /></div>
|
||||||
|
<div class={!c.privacy_mode ? "disabled" : ""} title={translate('Enable privacy mode')} style={is_privacy_mode_supported ? "" : "display:none;"}><icon .privacy_mode /></div>
|
||||||
</div></div>
|
</div></div>
|
||||||
}
|
}
|
||||||
{c.is_file_transfer ? <div>{translate('Transfer file')}</div> : ""}
|
{c.is_file_transfer ? <div>{translate('Transfer file')}</div> : ""}
|
||||||
@@ -103,6 +108,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.keyboard) (e) {
|
event click $(icon.keyboard) (e) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.keyboard = !connection.keyboard;
|
connection.keyboard = !connection.keyboard;
|
||||||
@@ -112,6 +118,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.clipboard) {
|
event click $(icon.clipboard) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.clipboard = !connection.clipboard;
|
connection.clipboard = !connection.clipboard;
|
||||||
@@ -121,6 +128,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.audio) {
|
event click $(icon.audio) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.audio = !connection.audio;
|
connection.audio = !connection.audio;
|
||||||
@@ -130,6 +138,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.file) {
|
event click $(icon.file) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.file = !connection.file;
|
connection.file = !connection.file;
|
||||||
@@ -139,6 +148,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.restart) {
|
event click $(icon.restart) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.restart = !connection.restart;
|
connection.restart = !connection.restart;
|
||||||
@@ -148,6 +158,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.recording) {
|
event click $(icon.recording) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.recording = !connection.recording;
|
connection.recording = !connection.recording;
|
||||||
@@ -157,6 +168,7 @@ class Body: Reactor.Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
event click $(icon.block_input) {
|
event click $(icon.block_input) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
connection.block_input = !connection.block_input;
|
connection.block_input = !connection.block_input;
|
||||||
@@ -165,6 +177,16 @@ class Body: Reactor.Component
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event click $(icon.privacy_mode) {
|
||||||
|
if (!allow_perm_change_in_accept_window) return;
|
||||||
|
var { cid, connection } = this;
|
||||||
|
checkClickTime(function() {
|
||||||
|
connection.privacy_mode = !connection.privacy_mode;
|
||||||
|
body.update();
|
||||||
|
handler.switch_permission(cid, "privacy_mode", connection.privacy_mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
event click $(button#accept) {
|
event click $(button#accept) {
|
||||||
var { cid, connection } = this;
|
var { cid, connection } = this;
|
||||||
checkClickTime(function() {
|
checkClickTime(function() {
|
||||||
@@ -368,7 +390,7 @@ function bring_to_top(idx=-1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
|
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode) {
|
||||||
stdout.println("new connection #" + id + ": " + peer_id);
|
stdout.println("new connection #" + id + ": " + peer_id);
|
||||||
var conn;
|
var conn;
|
||||||
connections.map(function(c) {
|
connections.map(function(c) {
|
||||||
@@ -376,6 +398,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
|
|||||||
});
|
});
|
||||||
if (conn) {
|
if (conn) {
|
||||||
conn.authorized = authorized;
|
conn.authorized = authorized;
|
||||||
|
conn.privacy_mode = privacy_mode;
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -391,7 +414,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
|
|||||||
name: name, authorized: authorized, time: new Date(), now: new Date(),
|
name: name, authorized: authorized, time: new Date(), now: new Date(),
|
||||||
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
||||||
audio: audio, file: file, restart: restart, recording: recording,
|
audio: audio, file: file, restart: restart, recording: recording,
|
||||||
block_input:block_input,
|
block_input:block_input, privacy_mode:privacy_mode,
|
||||||
disconnected: false
|
disconnected: false
|
||||||
};
|
};
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
@@ -480,15 +503,21 @@ function getElapsed(time, now) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ui_status_cache = [""];
|
var ui_status_cache = ["", ""];
|
||||||
function check_update_ui() {
|
function check_update_ui() {
|
||||||
self.timer(1s, function() {
|
self.timer(1s, function() {
|
||||||
var approve_mode = handler.get_option('approve-mode');
|
var approve_mode = handler.get_option('approve-mode');
|
||||||
|
var allow_perm_change = handler.get_builtin_option('enable-perm-change-in-accept-window');
|
||||||
var changed = false;
|
var changed = false;
|
||||||
if (ui_status_cache[0] != approve_mode) {
|
if (ui_status_cache[0] != approve_mode) {
|
||||||
ui_status_cache[0] = approve_mode;
|
ui_status_cache[0] = approve_mode;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
if (ui_status_cache[1] != allow_perm_change) {
|
||||||
|
ui_status_cache[1] = allow_perm_change;
|
||||||
|
allow_perm_change_in_accept_window = allow_perm_change != 'N';
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
if (changed) update();
|
if (changed) update();
|
||||||
check_update_ui();
|
check_update_ui();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class Header: Reactor.Component {
|
|||||||
{is_file_copy_paste_supported && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</li> : ""}
|
{is_file_copy_paste_supported && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</li> : ""}
|
||||||
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
|
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
|
||||||
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
|
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
|
||||||
{keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
|
{(pi.platform == "Windows" || pi.platform == "Mac OS") && (handler.get_toggle_option("privacy-mode") || (keyboard_enabled && privacy_mode_enabled)) ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
|
||||||
{keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ? <li #allow_swap_key .toggle-option><span>{svg_checkmark}</span>{translate('Swap control-command key')}</li> : ""}
|
{keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ? <li #allow_swap_key .toggle-option><span>{svg_checkmark}</span>{translate('Swap control-command key')}</li> : ""}
|
||||||
{handler.version_cmp(pi.version, '1.2.4') >= 0 ? <li #i444><span>{svg_checkmark}</span>{translate('True color (4:4:4)')}</li> : ""}
|
{handler.version_cmp(pi.version, '1.2.4') >= 0 ? <li #i444><span>{svg_checkmark}</span>{translate('True color (4:4:4)')}</li> : ""}
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -521,6 +521,7 @@ class MyIdMenu: Reactor.Component {
|
|||||||
{!disable_settings && <li #enable-remote-restart><span>{svg_checkmark}</span>{translate('Enable remote restart')}</li>}
|
{!disable_settings && <li #enable-remote-restart><span>{svg_checkmark}</span>{translate('Enable remote restart')}</li>}
|
||||||
{!disable_settings && <li #enable-tunnel><span>{svg_checkmark}</span>{translate('Enable TCP tunneling')}</li>}
|
{!disable_settings && <li #enable-tunnel><span>{svg_checkmark}</span>{translate('Enable TCP tunneling')}</li>}
|
||||||
{!disable_settings && is_win ? <li #enable-block-input><span>{svg_checkmark}</span>{translate('Enable blocking user input')}</li> : ""}
|
{!disable_settings && is_win ? <li #enable-block-input><span>{svg_checkmark}</span>{translate('Enable blocking user input')}</li> : ""}
|
||||||
|
{!disable_settings && (handler.get_supported_privacy_mode_impls() != '[]') && <li #enable-privacy-mode><span>{svg_checkmark}</span>{translate('Enable privacy mode')}</li>}
|
||||||
{!disable_settings && <li #enable-lan-discovery><span>{svg_checkmark}</span>{translate('Enable LAN discovery')}</li>}
|
{!disable_settings && <li #enable-lan-discovery><span>{svg_checkmark}</span>{translate('Enable LAN discovery')}</li>}
|
||||||
<AudioInputs />
|
<AudioInputs />
|
||||||
<Enhancements />
|
<Enhancements />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var audio_enabled = true; // server side
|
|||||||
var file_enabled = true; // server side
|
var file_enabled = true; // server side
|
||||||
var restart_enabled = true; // server side
|
var restart_enabled = true; // server side
|
||||||
var recording_enabled = true; // server side
|
var recording_enabled = true; // server side
|
||||||
|
var privacy_mode_enabled = true; // server side
|
||||||
var scroll_body = $(body);
|
var scroll_body = $(body);
|
||||||
var peer_platform = "";
|
var peer_platform = "";
|
||||||
|
|
||||||
@@ -588,6 +589,7 @@ handler.setPermission = function(name, enabled) {
|
|||||||
if (name == "clipboard") clipboard_enabled = enabled;
|
if (name == "clipboard") clipboard_enabled = enabled;
|
||||||
if (name == "restart") restart_enabled = enabled;
|
if (name == "restart") restart_enabled = enabled;
|
||||||
if (name == "recording") recording_enabled = enabled;
|
if (name == "recording") recording_enabled = enabled;
|
||||||
|
if (name == "privacy_mode") privacy_mode_enabled = enabled;
|
||||||
input_blocked = false;
|
input_blocked = false;
|
||||||
header.update();
|
header.update();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ use hbb_common::fs::serialize_transfer_job;
|
|||||||
use hbb_common::tokio::sync::mpsc::unbounded_channel;
|
use hbb_common::tokio::sync::mpsc::unbounded_channel;
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
allow_err, bail,
|
allow_err, bail,
|
||||||
config::{keys::OPTION_FILE_TRANSFER_MAX_FILES, Config},
|
config::{
|
||||||
|
keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES},
|
||||||
|
option2bool, Config,
|
||||||
|
},
|
||||||
fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult},
|
fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult},
|
||||||
log,
|
log,
|
||||||
message_proto::*,
|
message_proto::*,
|
||||||
@@ -25,10 +28,7 @@ use hbb_common::{
|
|||||||
ResultType,
|
ResultType,
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use hbb_common::{
|
use hbb_common::{config::keys::*, tokio::sync::Mutex as TokioMutex};
|
||||||
config::{keys::*, option2bool},
|
|
||||||
tokio::sync::Mutex as TokioMutex,
|
|
||||||
};
|
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
|
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
@@ -143,6 +143,7 @@ pub struct Client {
|
|||||||
pub restart: bool,
|
pub restart: bool,
|
||||||
pub recording: bool,
|
pub recording: bool,
|
||||||
pub block_input: bool,
|
pub block_input: bool,
|
||||||
|
pub privacy_mode: bool,
|
||||||
pub from_switch: bool,
|
pub from_switch: bool,
|
||||||
pub in_voice_call: bool,
|
pub in_voice_call: bool,
|
||||||
pub incoming_voice_call: bool,
|
pub incoming_voice_call: bool,
|
||||||
@@ -230,6 +231,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
|||||||
restart: bool,
|
restart: bool,
|
||||||
recording: bool,
|
recording: bool,
|
||||||
block_input: bool,
|
block_input: bool,
|
||||||
|
privacy_mode: bool,
|
||||||
from_switch: bool,
|
from_switch: bool,
|
||||||
#[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender<Data>,
|
#[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender<Data>,
|
||||||
) {
|
) {
|
||||||
@@ -251,6 +253,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
|||||||
restart,
|
restart,
|
||||||
recording,
|
recording,
|
||||||
block_input,
|
block_input,
|
||||||
|
privacy_mode,
|
||||||
from_switch,
|
from_switch,
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
tx,
|
tx,
|
||||||
@@ -392,6 +395,23 @@ pub fn send_chat(id: i32, text: String) {
|
|||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
pub fn switch_permission(id: i32, name: String, enabled: bool) {
|
pub fn switch_permission(id: i32, name: String, enabled: bool) {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let is_keyboard_permission = name == "keyboard";
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let is_keyboard_permission = false;
|
||||||
|
if !option2bool(
|
||||||
|
OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
) && !is_keyboard_permission
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"blocked cm switch_permission by policy, conn_id={}, permission={}, enabled={}",
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||||
allow_err!(client.tx.send(Data::SwitchPermission { name, enabled }));
|
allow_err!(client.tx.send(Data::SwitchPermission { name, enabled }));
|
||||||
};
|
};
|
||||||
@@ -400,6 +420,19 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) {
|
|||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub fn switch_permission_all(name: String, enabled: bool) {
|
pub fn switch_permission_all(name: String, enabled: bool) {
|
||||||
|
if name != "keyboard"
|
||||||
|
&& !option2bool(
|
||||||
|
OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"blocked cm switch_permission_all by policy, permission={}, enabled={}",
|
||||||
|
name,
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (_, client) in CLIENTS.read().unwrap().iter() {
|
for (_, client) in CLIENTS.read().unwrap().iter() {
|
||||||
allow_err!(client.tx.send(Data::SwitchPermission {
|
allow_err!(client.tx.send(Data::SwitchPermission {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
@@ -422,9 +455,16 @@ pub fn get_clients_length() -> usize {
|
|||||||
clients.len()
|
clients.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn has_active_clients() -> bool {
|
||||||
|
let clients = CLIENTS.read().unwrap();
|
||||||
|
clients.values().any(|c| !c.disconnected)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn switch_back(id: i32) {
|
pub fn switch_back(id: i32) {
|
||||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||||
allow_err!(client.tx.send(Data::SwitchSidesBack));
|
allow_err!(client.tx.send(Data::SwitchSidesBack));
|
||||||
@@ -503,9 +543,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
|||||||
}
|
}
|
||||||
Ok(Some(data)) => {
|
Ok(Some(data)) => {
|
||||||
match data {
|
match data {
|
||||||
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
|
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, privacy_mode, from_switch} => {
|
||||||
log::debug!("conn_id: {}", id);
|
log::debug!("conn_id: {}", id);
|
||||||
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
|
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode, from_switch, self.tx.clone());
|
||||||
self.conn_id = id;
|
self.conn_id = id;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -533,6 +573,26 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
|||||||
Data::ChatMessage { text } => {
|
Data::ChatMessage { text } => {
|
||||||
self.cm.new_message(self.conn_id, text);
|
self.cm.new_message(self.conn_id, text);
|
||||||
}
|
}
|
||||||
|
Data::SwitchPermission { name, enabled } => {
|
||||||
|
// Keep this branch scoped to privacy mode rollback.
|
||||||
|
// Other CM permission toggles are updated optimistically by the UI itself.
|
||||||
|
// The backend currently sends SwitchPermission back to CM only when
|
||||||
|
// privacy-mode turn-off fails and the UI state must be restored.
|
||||||
|
if name == "privacy_mode" {
|
||||||
|
let client = {
|
||||||
|
let mut clients = CLIENTS.write().unwrap();
|
||||||
|
clients.get_mut(&self.conn_id).map(|c| {
|
||||||
|
c.privacy_mode = enabled;
|
||||||
|
c.clone()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(client) = client {
|
||||||
|
// This reuses add_connection(), and cm.tis only selectively updates
|
||||||
|
// existing rows (authorized/privacy_mode) for this fallback path.
|
||||||
|
self.cm.ui_handler.add_connection(&client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Data::FS(mut fs) => {
|
Data::FS(mut fs) => {
|
||||||
if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs {
|
if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs {
|
||||||
if let Ok(bytes) = self.stream.next_raw().await {
|
if let Ok(bytes) = self.stream.next_raw().await {
|
||||||
@@ -835,6 +895,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
|||||||
restart,
|
restart,
|
||||||
recording,
|
recording,
|
||||||
block_input,
|
block_input,
|
||||||
|
privacy_mode,
|
||||||
from_switch,
|
from_switch,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
@@ -856,6 +917,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
|||||||
restart,
|
restart,
|
||||||
recording,
|
recording,
|
||||||
block_input,
|
block_input,
|
||||||
|
privacy_mode,
|
||||||
from_switch,
|
from_switch,
|
||||||
tx.clone(),
|
tx.clone(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -870,12 +870,14 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn enter(&self, keyboard_mode: String) {
|
pub fn enter(&self, keyboard_mode: String) {
|
||||||
keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode);
|
let session_id = self.lc.read().unwrap().session_id as u128;
|
||||||
|
keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode, session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn leave(&self, keyboard_mode: String) {
|
pub fn leave(&self, keyboard_mode: String) {
|
||||||
keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode);
|
let session_id = self.lc.read().unwrap().session_id as u128;
|
||||||
|
keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode, session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// flutter only TODO new input
|
// flutter only TODO new input
|
||||||
@@ -1462,10 +1464,11 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
self.send(Data::ElevateWithLogon(username, password));
|
self.send(Data::ElevateWithLogon(username, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "ios"))]
|
#[cfg(any(target_os = "android", target_os = "ios", not(feature = "flutter")))]
|
||||||
pub fn switch_sides(&self) {}
|
pub fn switch_sides(&self) {}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
pub async fn switch_sides(&self) {
|
pub async fn switch_sides(&self) {
|
||||||
match crate::ipc::connect(1000, "").await {
|
match crate::ipc::connect(1000, "").await {
|
||||||
|
|||||||
Reference in New Issue
Block a user