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:
2026-09-22 21:46:01 +03:00
parent 594a5c0af5
commit cffcec72b8
52 changed files with 8659 additions and 16 deletions
+128
View File
@@ -0,0 +1,128 @@
//! Команды обёртки: типизированный мост «веб-клиент → Rust» (docs/client-tauri.md §2).
//!
//! Команд ровно столько, сколько нужно странице: прочитать и сохранить
//! настройки, сообщить о готовности, показать уведомление, обновить счётчик
//! непрочитанных, открыть настройки/клиент и проверить обновления. Никакой
//! бизнес-логики здесь нет — только платформенные действия.
use tauri::{AppHandle, Manager, Runtime};
use crate::bridge::{self, WebMessage};
use crate::settings::Settings;
use crate::state::AppState;
use crate::{autostart, instance, notifications, shortcuts, updates, window};
/// Текущие настройки обёртки.
#[tauri::command]
pub fn desktop_settings<R: Runtime>(app: AppHandle<R>) -> Settings {
app.state::<AppState>().settings()
}
/// Версия приложения — показывается на странице настроек.
#[tauri::command]
pub fn desktop_version<R: Runtime>(app: AppHandle<R>) -> String {
app.package_info().version.to_string()
}
/// Сохраняет настройки и применяет их: автозапуск, шорткаты, адрес инстанса.
#[tauri::command]
pub fn desktop_save_settings<R: Runtime>(
app: AppHandle<R>,
settings: Settings,
) -> Result<Settings, String> {
// Адрес проверяем до применения, чтобы не оставить обёртку без клиента.
let url = instance::normalize(&settings.instance_url)?;
let mut next = settings;
next.instance_url = instance::display(&url);
let previous = app.state::<AppState>().settings();
// Шорткаты применяем первыми: если комбинация занята, настройки не сохраняем.
if next.ptt_shortcut != previous.ptt_shortcut
|| next.mute_shortcut != previous.mute_shortcut
|| next.deafen_shortcut != previous.deafen_shortcut
{
if let Err(error) = shortcuts::apply(&app, &next) {
let _ = shortcuts::apply(&app, &previous);
return Err(error);
}
}
if let Err(error) = autostart::apply(&app, next.autostart) {
log::warn!("{error}");
}
app.state::<AppState>().set_settings(next.clone());
crate::settings::save(&app, &next)?;
crate::tray::refresh(&app);
announce_settings(&app, &next);
if next.instance_url != previous.instance_url {
window::open_instance_page(&app)?;
}
Ok(next)
}
/// Веб-клиент загрузился: отдаём накопленные deep link.
#[tauri::command]
pub fn desktop_ready<R: Runtime>(app: AppHandle<R>) {
let pending = app.state::<AppState>().mark_web_ready();
for url in pending {
bridge::dispatch(&app, &WebMessage::DeepLink { url });
}
}
/// Показывает системное уведомление.
#[tauri::command]
pub fn desktop_notify<R: Runtime>(
app: AppHandle<R>,
payload: notifications::NotifyPayload,
) -> Result<(), String> {
notifications::show(&app, &payload)
}
/// Обновляет счётчик непрочитанных в доке и трее.
#[tauri::command]
pub fn desktop_set_badge<R: Runtime>(app: AppHandle<R>, count: u32) {
notifications::set_badge(&app, count);
}
/// Открывает локальную страницу настроек обёртки.
#[tauri::command]
pub fn desktop_open_settings<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
window::open_settings_page(&app)
}
/// Возвращает окно к клиенту инстанса.
#[tauri::command]
pub fn desktop_open_instance<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
window::open_instance_page(&app)
}
/// Перезагружает клиент инстанса.
#[tauri::command]
pub fn desktop_reload<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
window::reload_instance(&app)
}
/// Проверяет обновления (кнопка на странице настроек).
#[tauri::command]
pub async fn desktop_check_updates<R: Runtime>(app: AppHandle<R>) {
updates::check(app, true).await;
}
/// Открывает ссылку в системном браузере.
#[tauri::command]
pub fn desktop_open_external<R: Runtime>(app: AppHandle<R>, url: String) -> Result<(), String> {
let parsed = instance::normalize(&url)?;
tauri_plugin_opener::OpenerExt::opener(&app)
.open_url(parsed.as_str(), None::<&str>)
.map_err(|error| format!("не удалось открыть ссылку: {error}"))
}
/// Сообщает веб-клиенту, что настройки обёртки изменились.
pub fn announce_settings<R: Runtime>(app: &AppHandle<R>, settings: &Settings) {
bridge::send(
app,
&WebMessage::Settings {
settings: settings.clone(),
},
);
}