72c5189e8f
Три пункта спецификации клиента (docs/client-tauri.md §5), которые оставались открытыми в обёртке: - **Клик по уведомлению → переход в канал.** У `tauri-plugin-notification` на desktop нет колбэка действия, поэтому реализовано лучшее доступное поведение: уведомление, показанное при неактивном окне, запоминается, и когда приложение активируется в течение 10 секунд (окно получило фокус или macOS прислала `RunEvent::Reopen`), окно поднимается и открывается комната из уведомления — `glchat://guild/<id>/channel/<id>`, для личной беседы `glchat://dm/<id>`. Идентификаторы проверяются (только цифры), чтобы из текста уведомления нельзя было собрать произвольный deep link. Ограничение: ОС не сообщает, что активация вызвана именно кликом, поэтому переход сработает и при обычном возврате в приложение в эти 10 секунд — это описано в desktop/README.md; - **Выход при несохранённых изменениях.** Клиент сообщает о правках командой `desktop_set_dirty`; закрытие окна и пункт «Выход» в трее перехватываются (`window::request_close_confirmation`), окно best-effort покачивается (`Window::set_position`, на Wayland молча пропускается), а решение принимает пользователь в диалоге клиента «Сохранить / Выйти без сохранения»; закрывает приложение команда `desktop_close_window`. Раньше выход из трея терял правки молча; - **Ctrl/Cmd+K.** Комбинация регистрируется глобально (`shortcuts.rs`) и при неактивном окне поднимает окно и просит клиент открыть палитру команд (`WebMessage::OpenPalette`); повторы комбинаций с настраиваемыми шорткатами отсекаются по разобранной комбинации, иначе вторая регистрация падала бы. Заодно: `updates.rs` получил тест, который разбирает манифест релиза типом самого `tauri-plugin-updater` (`RemoteRelease`) — он ловит расхождение формата (например, дату не в RFC 3339), из-за которого обновление молча не приходило бы. Проверки: `make desktop-check` (fmt, clippy, 10 тестов).
75 lines
3.8 KiB
Rust
75 lines
3.8 KiB
Rust
//! Мост «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 },
|
||
/// Обёртка перехватила закрытие окна: в клиенте есть несохранённые
|
||
/// изменения, он показывает тряску и диалог «Сохранить / Выйти без
|
||
/// сохранения» (docs/client-tauri.md §5).
|
||
CloseRequested,
|
||
/// Открыть палитру команд: глобальный Ctrl/Cmd+K при неактивном окне.
|
||
OpenPalette,
|
||
}
|
||
|
||
/// Отправляет сообщение в веб-клиент.
|
||
///
|
||
/// Если клиент ещё не загрузился, 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
|
||
}
|
||
}
|
||
}
|