Files
glchat/desktop/src-tauri/src/tray.rs
T

164 lines
6.7 KiB
Rust
Raw Normal View History

//! Иконка в трее: индикатор непрочитанных и меню управления обёрткой
//! (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<R: Runtime>(app: &AppHandle<R>) -> 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<R: Runtime>(app: &AppHandle<R>) {
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<R: Runtime>(app: &AppHandle<R>) -> Result<Menu<R>, String> {
let current = app.state::<AppState>().settings();
let item = |id: &str, title: &str| -> Result<MenuItem<R>, 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<CheckMenuItem<R>, 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,
&notifications,
&update_item,
&second_sep,
&quit,
],
)
.map_err(|error| format!("не удалось собрать меню трея: {error}"))
}
/// Обработчик пунктов меню трея.
fn on_menu_event<R: Runtime>(app: &AppHandle<R>, 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::<AppState>().settings();
current.autostart = !current.autostart;
persist(app, current);
}
"close-to-tray" => {
let mut current = app.state::<AppState>().settings();
current.close_to_tray = !current.close_to_tray;
persist(app, current);
}
"notifications" => {
let mut current = app.state::<AppState>().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<R: Runtime>(app: &AppHandle<R>, next: settings::Settings) {
let previous = app.state::<AppState>().settings();
app.state::<AppState>().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}");
}
}
}