diff --git a/README.md b/README.md index 1877632..3d981f9 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ ## Установка готового релиза -Скачайте `MiniVLESS_0.1.0_amd64.deb` из Releases и выполните из каталога загрузки: +Скачайте `MiniVLESS_0.1.1_amd64.deb` из Releases и выполните из каталога загрузки: ```bash -sudo apt install ./MiniVLESS_0.1.0_amd64.deb libcap2-bin +sudo apt install ./MiniVLESS_0.1.1_amd64.deb libcap2-bin sudo setcap cap_net_admin,cap_net_raw+ep /usr/lib/MiniVLESS/binaries/sing-box ``` @@ -25,6 +25,9 @@ sudo setcap cap_net_admin,cap_net_raw+ep /usr/lib/MiniVLESS/binaries/sing-box - **Tunneling выключен:** VPN для всей системы, независимо от списка программ. 3. `Refresh` обновляет запущенные приложения. `Add application` позволяет выбрать установленную программу или указать абсолютный путь к её настоящему ELF-бинарнику. 4. Нажмите `Connect`. `Disconnect` останавливает core и освобождает TUN. +5. Кнопка **−** в заголовке скрывает окно в системный трей, сохраняя подключение. В меню значка доступны **Show MiniVLESS**, **Disconnect** и **Quit**. Крестик и Quit полностью завершают приложение и останавливают VPN. + +Если системный трей недоступен (например, GNOME без расширения AppIndicator), кнопка − сворачивает окно в панель задач, откуда его можно вернуть. Для значка трея требуется поддержка StatusNotifier/AppIndicator в рабочем окружении. Пути сохраняются независимо от PID. Несколько процессов одного исполняемого файла объединяются. Отмеченные/добавленные приложения остаются в списке после перезапуска. Переключатель меняет **выборочную маршрутизацию**, а не отключает TUN: TUN используется в обоих режимах. diff --git a/package-lock.json b/package-lock.json index 1f8230b..c11c9c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "minivless", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "minivless", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@tauri-apps/api": "^2.11.0", "react": "^19.2.0", diff --git a/package.json b/package.json index 78225be..475fa90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minivless", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 33973ff..8f6bcec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1804,9 +1804,10 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minivless" -version = "0.1.0" +version = "0.1.1" dependencies = [ "base64 0.22.1", + "dbus", "libc", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fca7ca5..31f65ca 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minivless" -version = "0.1.0" +version = "0.1.1" description = "A small personal VLESS client for Linux" edition = "2021" license = "MIT" @@ -8,13 +8,14 @@ rust-version = "1.85" [features] default = ["desktop"] -desktop = ["dep:tauri", "dep:tauri-build"] +desktop = ["dep:tauri", "dep:tauri-build", "dep:dbus"] [build-dependencies] tauri-build = { version = "2", optional = true , features = [] } [dependencies] -tauri = { version = "2", optional = true , features = [] } +tauri = { version = "2", optional = true , features = ["tray-icon"] } +dbus = { version = "0.9", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5772d1a..7adecaa 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,7 +10,83 @@ use std::{path::PathBuf, sync::Arc}; mod desktop { use super::*; use std::sync::atomic::{AtomicBool, Ordering}; - use tauri::{Manager, State}; + use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, + Manager, State, + }; + + struct TrayReady(bool); + + fn tray_host_available() -> bool { + use std::time::Duration; + let Ok(connection) = dbus::blocking::Connection::new_session() else { + return false; + }; + let proxy = connection.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_millis(300), + ); + let reply: Result<(bool,), _> = proxy.method_call( + "org.freedesktop.DBus", + "NameHasOwner", + ("org.kde.StatusNotifierWatcher",), + ); + matches!(reply, Ok((true,))) + } + + #[tauri::command] + async fn hide_window( + app: tauri::AppHandle, + window: tauri::WebviewWindow, + ) -> Result<(), String> { + let tray_ready = app.state::().0; + let has_host = tray_ready + && tauri::async_runtime::spawn_blocking(tray_host_available) + .await + .unwrap_or(false); + // A tray icon can be created even when GNOME has no indicator extension. + // Keep the window recoverable from the taskbar in that case. + if has_host { + window.hide() + } else { + window.minimize() + } + .map_err(|_| "Cannot hide the window".into()) + } + + fn create_tray(app: &tauri::App) -> tauri::Result<()> { + let show = MenuItem::with_id(app, "show", "Show MiniVLESS", true, None::<&str>)?; + let disconnect = MenuItem::with_id(app, "disconnect", "Disconnect", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &disconnect, &quit])?; + let mut tray = TrayIconBuilder::with_id("minivless-tray") + .menu(&menu) + .tooltip("MiniVLESS"); + if let Some(icon) = app.default_window_icon() { + tray = tray.icon(icon.clone()); + } + tray.on_menu_event(|app, event| match event.id.as_ref() { + "show" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + "disconnect" => app.state::>().disconnect(), + // Reuse the existing CloseRequested handler, which awaits core shutdown. + "quit" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.close(); + } + } + _ => {} + }) + .build(app)?; + Ok(()) + } #[tauri::command] async fn list_applications() -> Result, String> { tauri::async_runtime::spawn_blocking(processes::list_running) @@ -79,7 +155,8 @@ mod desktop { get_status, get_logs, load_preferences, - save_preferences + save_preferences, + hide_window ]) .setup(move |app| { let resources = app.path().resource_dir().ok(); @@ -89,6 +166,8 @@ mod desktop { std::env::current_exe()?, ); app.manage(state.clone()); + // Lack of tray support must never prevent the VPN client from starting. + app.manage(TrayReady(create_tray(app).is_ok())); let signals = Arc::new(AtomicBool::new(false)); for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] { signal_hook::flag::register(sig, signals.clone())?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5556bae..84c857a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "MiniVLESS", - "version": "0.1.0", + "version": "0.1.1", "identifier": "dev.minivless.desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 0d1bd60..7e96154 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,11 @@ const initialStatus: Status = { const message = (e: unknown) => typeof e === "string" ? e : "Something went wrong. Please try again."; const basename = (p: string) => p.split("/").pop() || p; -function Icon({ kind }: { kind: "close" | "eye" | "refresh" | "plus" }) { +function Icon({ + kind, +}: { + kind: "close" | "eye" | "refresh" | "plus" | "hide"; +}) { return ( MiniVLESS - +
+ + +
{!desktop && ( diff --git a/src/styles.css b/src/styles.css index fc66882..30c84f9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -58,6 +58,11 @@ button svg { font-size: 15px; font-weight: 500; } +.window-actions { + display: flex; + align-items: center; + gap: 2px; +} .icon-button { display: inline-flex; align-items: center;