Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50a2f49e7c |
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
## Установка готового релиза
|
## Установка готового релиза
|
||||||
|
|
||||||
Скачайте `MiniVLESS_0.1.0_amd64.deb` из Releases и выполните из каталога загрузки:
|
Скачайте `MiniVLESS_0.1.1_amd64.deb` из Releases и выполните из каталога загрузки:
|
||||||
|
|
||||||
```bash
|
```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
|
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 для всей системы, независимо от списка программ.
|
- **Tunneling выключен:** VPN для всей системы, независимо от списка программ.
|
||||||
3. `Refresh` обновляет запущенные приложения. `Add application` позволяет выбрать установленную программу или указать абсолютный путь к её настоящему ELF-бинарнику.
|
3. `Refresh` обновляет запущенные приложения. `Add application` позволяет выбрать установленную программу или указать абсолютный путь к её настоящему ELF-бинарнику.
|
||||||
4. Нажмите `Connect`. `Disconnect` останавливает core и освобождает TUN.
|
4. Нажмите `Connect`. `Disconnect` останавливает core и освобождает TUN.
|
||||||
|
5. Кнопка **−** в заголовке скрывает окно в системный трей, сохраняя подключение. В меню значка доступны **Show MiniVLESS**, **Disconnect** и **Quit**. Крестик и Quit полностью завершают приложение и останавливают VPN.
|
||||||
|
|
||||||
|
Если системный трей недоступен (например, GNOME без расширения AppIndicator), кнопка − сворачивает окно в панель задач, откуда его можно вернуть. Для значка трея требуется поддержка StatusNotifier/AppIndicator в рабочем окружении.
|
||||||
|
|
||||||
Пути сохраняются независимо от PID. Несколько процессов одного исполняемого файла объединяются. Отмеченные/добавленные приложения остаются в списке после перезапуска. Переключатель меняет **выборочную маршрутизацию**, а не отключает TUN: TUN используется в обоих режимах.
|
Пути сохраняются независимо от PID. Несколько процессов одного исполняемого файла объединяются. Отмеченные/добавленные приложения остаются в списке после перезапуска. Переключатель меняет **выборочную маршрутизацию**, а не отключает TUN: TUN используется в обоих режимах.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "minivless",
|
"name": "minivless",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "minivless",
|
"name": "minivless",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "minivless",
|
"name": "minivless",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-1
@@ -1804,9 +1804,10 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minivless"
|
name = "minivless"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"dbus",
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "minivless"
|
name = "minivless"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "A small personal VLESS client for Linux"
|
description = "A small personal VLESS client for Linux"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -8,13 +8,14 @@ rust-version = "1.85"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["desktop"]
|
default = ["desktop"]
|
||||||
desktop = ["dep:tauri", "dep:tauri-build"]
|
desktop = ["dep:tauri", "dep:tauri-build", "dep:dbus"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", optional = true , features = [] }
|
tauri-build = { version = "2", optional = true , features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[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 = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
|||||||
+81
-2
@@ -10,7 +10,83 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
mod desktop {
|
mod desktop {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
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]
|
#[tauri::command]
|
||||||
async fn list_applications() -> Result<Vec<ApplicationInfo>, String> {
|
async fn list_applications() -> Result<Vec<ApplicationInfo>, String> {
|
||||||
tauri::async_runtime::spawn_blocking(processes::list_running)
|
tauri::async_runtime::spawn_blocking(processes::list_running)
|
||||||
@@ -79,7 +155,8 @@ mod desktop {
|
|||||||
get_status,
|
get_status,
|
||||||
get_logs,
|
get_logs,
|
||||||
load_preferences,
|
load_preferences,
|
||||||
save_preferences
|
save_preferences,
|
||||||
|
hide_window
|
||||||
])
|
])
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let resources = app.path().resource_dir().ok();
|
let resources = app.path().resource_dir().ok();
|
||||||
@@ -89,6 +166,8 @@ mod desktop {
|
|||||||
std::env::current_exe()?,
|
std::env::current_exe()?,
|
||||||
);
|
);
|
||||||
app.manage(state.clone());
|
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));
|
let signals = Arc::new(AtomicBool::new(false));
|
||||||
for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
|
for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
|
||||||
signal_hook::flag::register(sig, signals.clone())?;
|
signal_hook::flag::register(sig, signals.clone())?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "MiniVLESS",
|
"productName": "MiniVLESS",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"identifier": "dev.minivless.desktop",
|
"identifier": "dev.minivless.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+36
-9
@@ -17,7 +17,11 @@ const initialStatus: Status = {
|
|||||||
const message = (e: unknown) =>
|
const message = (e: unknown) =>
|
||||||
typeof e === "string" ? e : "Something went wrong. Please try again.";
|
typeof e === "string" ? e : "Something went wrong. Please try again.";
|
||||||
const basename = (p: string) => p.split("/").pop() || p;
|
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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
@@ -30,7 +34,9 @@ function Icon({ kind }: { kind: "close" | "eye" | "refresh" | "plus" }) {
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{kind === "close" ? (
|
{kind === "hide" ? (
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
) : kind === "close" ? (
|
||||||
<path d="m6 6 12 12M18 6 6 18" />
|
<path d="m6 6 12 12M18 6 6 18" />
|
||||||
) : kind === "plus" ? (
|
) : kind === "plus" ? (
|
||||||
<path d="M12 5v14M5 12h14" />
|
<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) => {
|
const add = (app: ApplicationInfo) => {
|
||||||
setApps((old) => [
|
setApps((old) => [
|
||||||
...old.filter((a) => a.executable !== app.executable),
|
...old.filter((a) => a.executable !== app.executable),
|
||||||
@@ -233,13 +249,24 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>MiniVLESS</span>
|
<span>MiniVLESS</span>
|
||||||
<button
|
<div className="window-actions">
|
||||||
className="icon-button close"
|
<button
|
||||||
aria-label="Close MiniVLESS"
|
className="icon-button"
|
||||||
onClick={() => void close()}
|
aria-label="Hide MiniVLESS"
|
||||||
>
|
title="Hide window — VPN stays connected"
|
||||||
<Icon kind="close" />
|
onClick={() => void hide()}
|
||||||
</button>
|
disabled={!desktop}
|
||||||
|
>
|
||||||
|
<Icon kind="hide" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="icon-button close"
|
||||||
|
aria-label="Close MiniVLESS"
|
||||||
|
onClick={() => void close()}
|
||||||
|
>
|
||||||
|
<Icon kind="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{!desktop && (
|
{!desktop && (
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ button svg {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.window-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
.icon-button {
|
.icon-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user