feat(gui): закрытие окна при включённой защите сворачивает TGLock в трей; 2.0.0-beta.18 (#74)

Раньше закрытие окна завершало приложение вместе с прокси, и Telegram
терял соединение. Теперь, пока защита включена, окно прячется в трей,
а прокси продолжает работать, как в tg-ws-proxy. Меню трея: открыть
TGLock, открыть Telegram, выход. Левый клик по значку открывает окно
(в macOS меню по клику, окно возвращает и клик по значку в Dock).

Если защита выключена или значок в трее создать не удалось (в Linux
нет libayatana-appindicator3), окно закрывается как раньше, чтобы не
оставлять приложение без способа до него добраться.

Co-authored-by: babin <Thartewerner536e@engineer.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Никита Sonic
2026-10-26 19:48:04 +00:00
committed by GitHub
parent 7b48f0046b
commit 098178ead4
7 changed files with 114 additions and 10 deletions
Generated
+1 -1
View File
@@ -3822,7 +3822,7 @@ dependencies = [
[[package]] [[package]]
name = "tglock" name = "tglock"
version = "2.0.0-beta.17" version = "2.0.0-beta.18"
dependencies = [ dependencies = [
"aes", "aes",
"cipher", "cipher",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tglock" name = "tglock"
version = "2.0.0-beta.17" version = "2.0.0-beta.18"
edition = "2021" edition = "2021"
rust-version = "1.88" rust-version = "1.88"
description = "Telegram unblock via local WebSocket tunnel" description = "Telegram unblock via local WebSocket tunnel"
@@ -38,7 +38,7 @@ path = "src/bin/cli.rs"
required-features = ["cli"] required-features = ["cli"]
[dependencies] [dependencies]
tauri = { version = "2", features = [], optional = true } tauri = { version = "2", features = ["tray-icon"], optional = true }
tauri-plugin-opener = { version = "2", optional = true } tauri-plugin-opener = { version = "2", optional = true }
clap = { version = "4", features = ["derive"], optional = true } clap = { version = "4", features = ["derive"], optional = true }
# Файл конфигурации CLI. TOML, а не JSON, потому что файл правят руками и в нём # Файл конфигурации CLI. TOML, а не JSON, потому что файл правят руками и в нём
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "tglock-ui", "name": "tglock-ui",
"version": "2.0.0-beta.17", "version": "2.0.0-beta.18",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tglock-ui", "name": "tglock-ui",
"version": "2.0.0-beta.17", "version": "2.0.0-beta.18",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.8.0" "@tauri-apps/api": "^2.8.0"
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "tglock-ui", "name": "tglock-ui",
"private": true, "private": true,
"version": "2.0.0-beta.17", "version": "2.0.0-beta.18",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js", "test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js",
+106 -2
View File
@@ -431,8 +431,19 @@ pub fn run() {
); );
} }
app.manage(state); app.manage(state);
#[cfg(desktop)]
if let Err(error) = setup_tray(app) {
app.state::<AppState>().log(
format!("Значок в трее недоступен ({error}). Закрытие окна выключит защиту"),
true,
);
}
Ok(()) Ok(())
}) })
.on_window_event(|_window, _event| {
#[cfg(desktop)]
hide_to_tray_on_close(_window, _event);
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_status, get_status,
get_settings, get_settings,
@@ -442,8 +453,101 @@ pub fn run() {
get_telegram_link, get_telegram_link,
open_telegram open_telegram
]) ])
.run(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("failed to run TGLock"); .expect("failed to build TGLock")
.run(|_app, _event| {
// В macOS окно, спрятанное в трей, возвращает и клик по значку в Dock.
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Reopen { .. } = _event {
show_main_window(_app);
}
});
}
#[cfg(desktop)]
const TRAY_ID: &str = "main";
/// Закрытие окна при включённой защите прячет его в трей, и прокси продолжает
/// работать, как в tg-ws-proxy. Выйти можно из меню трея (by-sonic/tglock#70).
/// Если защита выключена или значка в трее нет, окно закрывается как раньше:
/// иначе приложение осталось бы работать без способа до него добраться.
#[cfg(desktop)]
fn hide_to_tray_on_close(window: &tauri::Window, event: &tauri::WindowEvent) {
let tauri::WindowEvent::CloseRequested { api, .. } = event else {
return;
};
let app = window.app_handle();
let running = app.state::<AppState>().stats.running.load(Ordering::SeqCst);
if running && app.tray_by_id(TRAY_ID).is_some() {
api.prevent_close();
let _ = window.hide();
}
}
#[cfg(desktop)]
fn show_main_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
#[cfg(desktop)]
fn setup_tray(app: &tauri::App) -> Result<(), String> {
// В Linux значок держится на libayatana-appindicator, и без неё tray-icon
// паникует, а не возвращает ошибку. Без трея окно просто закрывается.
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build_tray(app)))
.map_err(|_| "нет libayatana-appindicator3".to_string())?
.map_err(|error| error.to_string())
}
#[cfg(desktop)]
fn build_tray(app: &tauri::App) -> tauri::Result<()> {
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
let show = MenuItem::with_id(app, "show", "Открыть TGLock", true, None::<&str>)?;
let telegram = MenuItem::with_id(app, "telegram", "Открыть Telegram", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Выход", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &telegram, &separator, &quit])?;
let mut tray = TrayIconBuilder::with_id(TRAY_ID)
.tooltip("TGLock")
.menu(&menu)
// В Windows и Linux левый клик открывает окно, меню — по правому.
// В macOS меню по клику привычнее.
.show_menu_on_left_click(cfg!(target_os = "macos"))
.on_menu_event(|app, event| match event.id().as_ref() {
"show" => show_main_window(app),
"telegram" => {
let state = app.state::<AppState>();
if let Err(error) = open_telegram_link(app, &state) {
state.log(error, true);
}
}
"quit" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if cfg!(target_os = "macos") {
return;
}
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
});
if let Some(icon) = app.default_window_icon() {
tray = tray.icon(icon.clone());
}
tray.build(app)?;
Ok(())
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "TGLock", "productName": "TGLock",
"version": "2.0.0-beta.17", "version": "2.0.0-beta.18",
"identifier": "com.bysonic.tglock", "identifier": "com.bysonic.tglock",
"mainBinaryName": "tglock", "mainBinaryName": "tglock",
"build": { "build": {
+1 -1
View File
@@ -518,7 +518,7 @@ async function copyDiagnostics(): Promise<void> {
// Public reports exclude proxy links, worker domains and free-form logs. // Public reports exclude proxy links, worker domains and free-form logs.
const report = { const report = {
application: "TGLock", application: "TGLock",
version: "2.0.0-beta.17", version: "2.0.0-beta.18",
running: status.running, running: status.running,
activeConnections: status.activeConnections, activeConnections: status.activeConnections,
tunnels: status.tunnels, tunnels: status.tunnels,