//! Иконка в трее: индикатор непрочитанных и меню управления обёрткой //! (docs/client-tauri.md §2 — «открыть, mute, статус, выйти»). use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{AppHandle, Manager, Runtime}; use crate::state::AppState; use crate::{autostart, commands, settings, shortcuts, updates, window}; /// Идентификатор иконки в трее. pub const TRAY_ID: &str = "main"; /// Создаёт иконку в трее с меню по текущим настройкам. pub fn build(app: &AppHandle) -> Result<(), String> { let icon = app .default_window_icon() .cloned() .ok_or_else(|| "в сборке нет иконки приложения".to_string())?; TrayIconBuilder::with_id(TRAY_ID) .icon(icon) .icon_as_template(true) .tooltip("glchat") .menu(&menu(app)?) .show_menu_on_left_click(false) .on_menu_event(on_menu_event) .on_tray_icon_event(|tray, event| { // Левый клик по иконке — показать окно, меню остаётся на правом. if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { window::show_main(tray.app_handle()); } }) .build(app) .map_err(|error| format!("не удалось создать иконку в трее: {error}"))?; Ok(()) } /// Пересобирает меню: галочки должны совпадать с текущими настройками. pub fn refresh(app: &AppHandle) { let Some(tray) = app.tray_by_id(TRAY_ID) else { return; }; match menu(app) { Ok(menu) => { if let Err(error) = tray.set_menu(Some(menu)) { log::warn!("не удалось обновить меню трея: {error}"); } } Err(error) => log::warn!("не удалось собрать меню трея: {error}"), } } /// Собирает меню трея по текущим настройкам. fn menu(app: &AppHandle) -> Result, String> { let current = app.state::().settings(); let item = |id: &str, title: &str| -> Result, String> { MenuItem::with_id(app, id, title, true, None::<&str>) .map_err(|error| format!("пункт меню «{title}»: {error}")) }; let check = |id: &str, title: &str, checked: bool| -> Result, String> { CheckMenuItem::with_id(app, id, title, true, checked, None::<&str>) .map_err(|error| format!("пункт меню «{title}»: {error}")) }; let open = item("open", "Открыть glchat")?; let client = item("open-settings", "Настройки обёртки…")?; let first_sep = PredefinedMenuItem::separator(app).map_err(|error| error.to_string())?; let autostart_item = check("autostart", "Запускать при входе", current.autostart)?; let close_to_tray = check("close-to-tray", "Закрывать в трей", current.close_to_tray)?; let notifications = check("notifications", "Уведомления", current.notifications)?; let update_item = item("updates", "Проверить обновления…")?; let second_sep = PredefinedMenuItem::separator(app).map_err(|error| error.to_string())?; let quit = item("quit", "Выход")?; Menu::with_items( app, &[ &open, &client, &first_sep, &autostart_item, &close_to_tray, ¬ifications, &update_item, &second_sep, &quit, ], ) .map_err(|error| format!("не удалось собрать меню трея: {error}")) } /// Обработчик пунктов меню трея. fn on_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { match event.id().as_ref() { "open" => window::show_main(app), "open-settings" => { if let Err(error) = window::open_settings_page(app) { log::warn!("{error}"); } } "autostart" => { let mut current = app.state::().settings(); current.autostart = !current.autostart; persist(app, current); } "close-to-tray" => { let mut current = app.state::().settings(); current.close_to_tray = !current.close_to_tray; persist(app, current); } "notifications" => { let mut current = app.state::().settings(); current.notifications = !current.notifications; persist(app, current); } "updates" => { let handle = app.clone(); tauri::async_runtime::spawn(async move { updates::check(handle, true, false).await; }); } // Выход из трея — такой же выход, как закрытие окна: если в клиенте // есть несохранённые правки, спрашиваем пользователя, а не теряем их. "quit" => { if !window::request_close_confirmation(app) { app.exit(0); } } other => log::debug!("неизвестный пункт меню трея: {other}"), } } /// Применяет и сохраняет настройки, меняя только один переключатель. fn persist(app: &AppHandle, next: settings::Settings) { let previous = app.state::().settings(); app.state::().set_settings(next.clone()); if let Err(error) = autostart::apply(app, next.autostart) { log::warn!("{error}"); } 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) { log::warn!("{error}"); } } if let Err(error) = settings::save(app, &next) { log::warn!("не удалось сохранить настройки: {error}"); } refresh(app); commands::announce_settings(app, &next); if next.instance_url != previous.instance_url { if let Err(error) = window::open_instance_page(app) { log::warn!("{error}"); } } }