From 098178ead46645cf3cc6b3f066a8cca3b36eb1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20Sonic?= Date: Mon, 26 Oct 2026 19:48:04 +0000 Subject: [PATCH] =?UTF-8?q?feat(gui):=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D0=B8=D0=B5=20=D0=BE=D0=BA=D0=BD=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87=D1=91=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B5=20=D1=81=D0=B2?= =?UTF-8?q?=D0=BE=D1=80=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=20TGLock?= =?UTF-8?q?=20=D0=B2=20=D1=82=D1=80=D0=B5=D0=B9;=202.0.0-beta.18=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше закрытие окна завершало приложение вместе с прокси, и Telegram терял соединение. Теперь, пока защита включена, окно прячется в трей, а прокси продолжает работать, как в tg-ws-proxy. Меню трея: открыть TGLock, открыть Telegram, выход. Левый клик по значку открывает окно (в macOS меню по клику, окно возвращает и клик по значку в Dock). Если защита выключена или значок в трее создать не удалось (в Linux нет libayatana-appindicator3), окно закрывается как раньше, чтобы не оставлять приложение без способа до него добраться. Co-authored-by: babin Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 4 +- package-lock.json | 4 +- package.json | 2 +- src/gui.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++- tauri.conf.json | 2 +- ui/main.ts | 2 +- 7 files changed, 114 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49ee890..935e804 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3822,7 +3822,7 @@ dependencies = [ [[package]] name = "tglock" -version = "2.0.0-beta.17" +version = "2.0.0-beta.18" dependencies = [ "aes", "cipher", diff --git a/Cargo.toml b/Cargo.toml index 714a195..280c5e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tglock" -version = "2.0.0-beta.17" +version = "2.0.0-beta.18" edition = "2021" rust-version = "1.88" description = "Telegram unblock via local WebSocket tunnel" @@ -38,7 +38,7 @@ path = "src/bin/cli.rs" required-features = ["cli"] [dependencies] -tauri = { version = "2", features = [], optional = true } +tauri = { version = "2", features = ["tray-icon"], optional = true } tauri-plugin-opener = { version = "2", optional = true } clap = { version = "4", features = ["derive"], optional = true } # Файл конфигурации CLI. TOML, а не JSON, потому что файл правят руками и в нём diff --git a/package-lock.json b/package-lock.json index 93c3fe8..3c4c0b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tglock-ui", - "version": "2.0.0-beta.17", + "version": "2.0.0-beta.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tglock-ui", - "version": "2.0.0-beta.17", + "version": "2.0.0-beta.18", "dependencies": { "@tauri-apps/api": "^2.8.0" }, diff --git a/package.json b/package.json index 4b2f149..1f297ed 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tglock-ui", "private": true, - "version": "2.0.0-beta.17", + "version": "2.0.0-beta.18", "type": "module", "scripts": { "test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js", diff --git a/src/gui.rs b/src/gui.rs index 1f23650..5b6ca6d 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -431,8 +431,19 @@ pub fn run() { ); } app.manage(state); + #[cfg(desktop)] + if let Err(error) = setup_tray(app) { + app.state::().log( + format!("Значок в трее недоступен ({error}). Закрытие окна выключит защиту"), + true, + ); + } Ok(()) }) + .on_window_event(|_window, _event| { + #[cfg(desktop)] + hide_to_tray_on_close(_window, _event); + }) .invoke_handler(tauri::generate_handler![ get_status, get_settings, @@ -442,8 +453,101 @@ pub fn run() { get_telegram_link, open_telegram ]) - .run(tauri::generate_context!()) - .expect("failed to run TGLock"); + .build(tauri::generate_context!()) + .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::().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::(); + 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)] diff --git a/tauri.conf.json b/tauri.conf.json index 4d74d9d..5fcda31 100644 --- a/tauri.conf.json +++ b/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "TGLock", - "version": "2.0.0-beta.17", + "version": "2.0.0-beta.18", "identifier": "com.bysonic.tglock", "mainBinaryName": "tglock", "build": { diff --git a/ui/main.ts b/ui/main.ts index 96dec70..cdf8b1e 100644 --- a/ui/main.ts +++ b/ui/main.ts @@ -518,7 +518,7 @@ async function copyDiagnostics(): Promise { // Public reports exclude proxy links, worker domains and free-form logs. const report = { application: "TGLock", - version: "2.0.0-beta.17", + version: "2.0.0-beta.18", running: status.running, activeConnections: status.activeConnections, tunnels: status.tunnels,