feat(desktop): обёртка Tauri 2 вокруг веб-клиента
Первый инкремент Фазы 6 (docs/client-tauri.md): окно с тем же веб-клиентом (по умолчанию https://gl.mhspx.su, адрес настраивается), трей с меню и индикатором непрочитанных, нативные уведомления, глобальный push-to-talk и переключение микрофона/deafen, автозапуск, single-instance, сохранение геометрии окна, deep links glchat://…, автообновление по подписанному манифесту. Связь страницы с обёрткой — через платформенный адаптер (web/src/lib/platform.ts) и минимальный типизированный набор команд; в веб-клиенте появились системные уведомления об упоминаниях и личных сообщениях и переключатель в настройках. В web/src/stores/gateway.ts вместе с вызовом уведомления лежат правки по дефектам realtime (READY и сессии) из параллельной волны: файл коммитится целиком по договорённости с координатором.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
//! Глобальные горячие клавиши (docs/client-tauri.md §5).
|
||||
//!
|
||||
//! Push-to-talk работает при неактивном окне: системный перехват ловит нажатие
|
||||
//! и отпускание выбранной комбинации и передаёт состояние в веб-клиент, а тот
|
||||
//! включает микрофон (в самом клиенте должен быть выбран режим «Push-to-talk»).
|
||||
//! Дополнительно доступны переключение микрофона и «не слышу».
|
||||
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
||||
use crate::bridge::{self, WebMessage};
|
||||
use crate::settings::Settings;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации.
|
||||
pub fn on_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: &Shortcut, event: tauri_plugin_global_shortcut::ShortcutEvent) {
|
||||
let settings = app.state::<AppState>().settings();
|
||||
let pressed = event.state == ShortcutState::Pressed;
|
||||
|
||||
let ptt = parse(&settings.ptt_shortcut);
|
||||
if ptt.as_ref() == Some(shortcut) {
|
||||
bridge::send(app, &WebMessage::Ptt { active: pressed });
|
||||
return;
|
||||
}
|
||||
// Остальные шорткаты срабатывают по нажатию, чтобы не дублировать действие.
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
if parse(&settings.mute_shortcut).as_ref() == Some(shortcut) {
|
||||
bridge::send(app, &WebMessage::ToggleMic);
|
||||
return;
|
||||
}
|
||||
if parse(&settings.deafen_shortcut).as_ref() == Some(shortcut) {
|
||||
bridge::send(app, &WebMessage::ToggleDeafen);
|
||||
}
|
||||
}
|
||||
|
||||
/// Перерегистрирует комбинации после изменения настроек.
|
||||
pub fn apply<R: Runtime>(app: &AppHandle<R>, settings: &Settings) -> Result<(), String> {
|
||||
let shortcuts = app.global_shortcut();
|
||||
shortcuts
|
||||
.unregister_all()
|
||||
.map_err(|error| format!("не удалось снять старые шорткаты: {error}"))?;
|
||||
|
||||
for spec in [
|
||||
&settings.ptt_shortcut,
|
||||
&settings.mute_shortcut,
|
||||
&settings.deafen_shortcut,
|
||||
] {
|
||||
if spec.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
shortcuts
|
||||
.register(spec.as_str())
|
||||
.map_err(|error| format!("комбинация «{spec}» не зарегистрирована: {error}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Разбирает строку комбинации; пустая строка и мусор дают `None`.
|
||||
fn parse(spec: &str) -> Option<Shortcut> {
|
||||
let trimmed = spec.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
trimmed.parse::<Shortcut>().ok()
|
||||
}
|
||||
Reference in New Issue
Block a user