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,68 @@
|
||||
//! Мост «Rust → веб-клиент».
|
||||
//!
|
||||
//! Обратное направление (страница → обёртка) идёт через типизированные команды
|
||||
//! (`commands.rs`), а события платформы передаются вызовом единственной функции
|
||||
//! `window.__glchatDesktop.receive(...)` в самой странице. Отдельного канала
|
||||
//! событий нет намеренно: сообщений мало, а типы у них общие с `web/src/lib/platform.ts`.
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
use crate::settings::Settings;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Метка главного окна (единственное окно приложения).
|
||||
pub const WEB_WINDOW: &str = "main";
|
||||
|
||||
/// Сообщение веб-клиенту: то, что платформа сообщает странице.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum WebMessage {
|
||||
/// Нажат или отпущен глобальный push-to-talk.
|
||||
Ptt { active: bool },
|
||||
/// Переключить микрофон (глобальный шорткат).
|
||||
ToggleMic,
|
||||
/// Переключить «не слышу» (глобальный шорткат).
|
||||
ToggleDeafen,
|
||||
/// Открыть deep link: приглашение или сервер с комнатой.
|
||||
DeepLink { url: String },
|
||||
/// Настройки обёртки изменились (обновлена страница настроек).
|
||||
Settings { settings: Settings },
|
||||
/// Доступно обновление — клиент показывает уведомление в интерфейсе.
|
||||
UpdateAvailable { version: String },
|
||||
}
|
||||
|
||||
/// Отправляет сообщение в веб-клиент.
|
||||
///
|
||||
/// Если клиент ещё не загрузился, deep link ставится в очередь и будет отдан
|
||||
/// после `desktop_ready` (docs/client-tauri.md §5). Остальные сообщения в этот
|
||||
/// момент терять не страшно: они относятся к активному взаимодействию.
|
||||
pub fn send<R: Runtime>(app: &AppHandle<R>, message: &WebMessage) -> bool {
|
||||
let state = app.state::<AppState>();
|
||||
if !state.web_ready() {
|
||||
if let WebMessage::DeepLink { url } = message {
|
||||
state.push_deep_link(url.clone());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
dispatch(app, message)
|
||||
}
|
||||
|
||||
/// Немедленная доставка без проверки готовности клиента.
|
||||
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, message: &WebMessage) -> bool {
|
||||
let Some(window) = app.get_webview_window(WEB_WINDOW) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(payload) = serde_json::to_string(message) else {
|
||||
log::warn!("не удалось сериализовать сообщение веб-клиенту");
|
||||
return false;
|
||||
};
|
||||
let script = format!("window.__glchatDesktop && window.__glchatDesktop.receive({payload});");
|
||||
match window.eval(&script) {
|
||||
Ok(()) => true,
|
||||
Err(error) => {
|
||||
log::debug!("веб-клиент не принял сообщение: {error}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user