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,94 @@
|
||||
//! Настройки desktop-обёртки.
|
||||
//!
|
||||
//! Лежат в `tauri-plugin-store` (`settings.json` в каталоге конфигурации
|
||||
//! приложения). Обёртка хранит только то, что относится к платформе: адрес
|
||||
//! инстанса и переключатели поведения. Сессия целиком живёт в cookie webview
|
||||
//! (docs/client-tauri.md §2): токенов и паролей здесь нет.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
/// Файл хранилища в каталоге конфигурации приложения.
|
||||
const STORE_FILE: &str = "settings.json";
|
||||
/// Ключ, под которым лежит объект настроек.
|
||||
const STORE_KEY: &str = "settings";
|
||||
|
||||
/// Адрес стенда по умолчанию: тот же клиент, что открывается в браузере.
|
||||
pub const DEFAULT_INSTANCE: &str = "https://gl.mhspx.su";
|
||||
|
||||
/// Комбинация push-to-talk по умолчанию.
|
||||
///
|
||||
/// Голых клавиш (как `V` в браузерном PTT) в глобальном шорткате быть не может:
|
||||
/// системный перехват занял бы клавишу у всех приложений, поэтому по умолчанию
|
||||
/// комбинация с модификаторами, а пустая строка означает «выключено».
|
||||
pub const DEFAULT_PTT_SHORTCUT: &str = "CommandOrControl+Shift+Space";
|
||||
|
||||
/// Комбинация «выключить/включить микрофон» по умолчанию.
|
||||
pub const DEFAULT_MUTE_SHORTCUT: &str = "CommandOrControl+Shift+M";
|
||||
|
||||
/// Комбинация «не слышу» (deafen) по умолчанию.
|
||||
pub const DEFAULT_DEAFEN_SHORTCUT: &str = "CommandOrControl+Shift+D";
|
||||
|
||||
/// Настройки обёртки. Поля в camelCase — их читает и пишет страница настроек.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
/// Адрес инстанса, который загружается в главное окно.
|
||||
pub instance_url: String,
|
||||
/// Закрытие окна сворачивает приложение в трей.
|
||||
pub close_to_tray: bool,
|
||||
/// Запускать приложение при входе в систему (свёрнутым в трей).
|
||||
pub autostart: bool,
|
||||
/// Пользователю уже задавали вопрос про автозапуск при первом запуске.
|
||||
pub autostart_prompted: bool,
|
||||
/// Глобальный push-to-talk (пустая строка — выключен).
|
||||
pub ptt_shortcut: String,
|
||||
/// Глобальное переключение микрофона (пустая строка — выключено).
|
||||
pub mute_shortcut: String,
|
||||
/// Глобальное переключение «не слышу» (пустая строка — выключено).
|
||||
pub deafen_shortcut: String,
|
||||
/// Показывать нативные уведомления об упоминаниях и личных сообщениях.
|
||||
pub notifications: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
instance_url: DEFAULT_INSTANCE.to_string(),
|
||||
close_to_tray: true,
|
||||
autostart: false,
|
||||
autostart_prompted: false,
|
||||
ptt_shortcut: DEFAULT_PTT_SHORTCUT.to_string(),
|
||||
mute_shortcut: DEFAULT_MUTE_SHORTCUT.to_string(),
|
||||
deafen_shortcut: DEFAULT_DEAFEN_SHORTCUT.to_string(),
|
||||
notifications: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Читает настройки из хранилища; при отсутствии или порче — значения по умолчанию.
|
||||
pub fn load<R: Runtime>(app: &AppHandle<R>) -> Settings {
|
||||
let Ok(store) = app.store(STORE_FILE) else {
|
||||
log::warn!("не удалось открыть хранилище настроек, используются значения по умолчанию");
|
||||
return Settings::default();
|
||||
};
|
||||
let Some(value) = store.get(STORE_KEY) else {
|
||||
return Settings::default();
|
||||
};
|
||||
match serde_json::from_value::<Settings>(value) {
|
||||
Ok(settings) => settings,
|
||||
Err(error) => {
|
||||
log::warn!("настройки в хранилище повреждены ({error}), берём значения по умолчанию");
|
||||
Settings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Сохраняет настройки в хранилище.
|
||||
pub fn save<R: Runtime>(app: &AppHandle<R>, settings: &Settings) -> Result<(), String> {
|
||||
let store = app.store(STORE_FILE).map_err(|error| error.to_string())?;
|
||||
let value = serde_json::to_value(settings).map_err(|error| error.to_string())?;
|
||||
store.set(STORE_KEY, value);
|
||||
store.save().map_err(|error| error.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user