b53cf9d6e7
Установка обновления всегда спрашивала согласие диалогом, поэтому сквозная проверка «поверх старой версии» не автоматизировалась (в headless-прогоне кликнуть некому), а на управляемых машинах обновление ставит оператор, а не пользователь. С флагом `--update-now` обёртка проверяет обновление сразу при старте (не через 30 секунд) и ставит найденное без диалога, после чего перезапускается; при отсутствии обновления работает как обычно. По умолчанию флаг выключен — трей, страница настроек и фоновая проверка по-прежнему спрашивают согласие. Подпись артефакта проверяется в обоих режимах: флаг убирает вопрос, но не проверку. Ход тихого обновления виден в журнале, а в строку запуска добавлена версия сборки — иначе после перезапуска не понять, какая версия работает. Решение — D-077, приёмка — desktop/README.md («Локальная проверка»).
164 lines
6.7 KiB
Rust
164 lines
6.7 KiB
Rust
//! Иконка в трее: индикатор непрочитанных и меню управления обёрткой
|
|
//! (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,
|
|
¬ifications,
|
|
&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}");
|
|
}
|
|
}
|
|
}
|