Release 0.1.1: hide client in system tray
This commit is contained in:
@@ -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 используется в обоих режимах.
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minivless",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+2
-1
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
+81
-2
@@ -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::<TrayReady>().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::<Arc<AppState>>().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<Vec<ApplicationInfo>, 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())?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
+36
-9
@@ -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 (
|
||||
<svg
|
||||
width="18"
|
||||
@@ -30,7 +34,9 @@ function Icon({ kind }: { kind: "close" | "eye" | "refresh" | "plus" }) {
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{kind === "close" ? (
|
||||
{kind === "hide" ? (
|
||||
<path d="M5 12h14" />
|
||||
) : kind === "close" ? (
|
||||
<path d="m6 6 12 12M18 6 6 18" />
|
||||
) : kind === "plus" ? (
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
@@ -199,6 +205,16 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
};
|
||||
const hide = async () => {
|
||||
if (!desktop) return;
|
||||
setShown(false);
|
||||
try {
|
||||
await saveQueue.current;
|
||||
await invoke("hide_window");
|
||||
} catch (e) {
|
||||
setError(message(e));
|
||||
}
|
||||
};
|
||||
const add = (app: ApplicationInfo) => {
|
||||
setApps((old) => [
|
||||
...old.filter((a) => a.executable !== app.executable),
|
||||
@@ -233,13 +249,24 @@ export default function App() {
|
||||
}}
|
||||
>
|
||||
<span>MiniVLESS</span>
|
||||
<button
|
||||
className="icon-button close"
|
||||
aria-label="Close MiniVLESS"
|
||||
onClick={() => void close()}
|
||||
>
|
||||
<Icon kind="close" />
|
||||
</button>
|
||||
<div className="window-actions">
|
||||
<button
|
||||
className="icon-button"
|
||||
aria-label="Hide MiniVLESS"
|
||||
title="Hide window — VPN stays connected"
|
||||
onClick={() => void hide()}
|
||||
disabled={!desktop}
|
||||
>
|
||||
<Icon kind="hide" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button close"
|
||||
aria-label="Close MiniVLESS"
|
||||
onClick={() => void close()}
|
||||
>
|
||||
<Icon kind="close" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
{!desktop && (
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user