diff --git a/desktop-client/src-tauri/Cargo.toml b/desktop-client/src-tauri/Cargo.toml index c932a8c..8b56a53 100644 --- a/desktop-client/src-tauri/Cargo.toml +++ b/desktop-client/src-tauri/Cargo.toml @@ -18,5 +18,5 @@ rand_core = { version = "0.6", features = ["getrandom"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tauri = { version = "2.10.1", features = [] } +tauri = { version = "2.10.1", features = ["tray-icon"] } x25519-dalek = { version = "2.0", features = ["static_secrets"] } diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 35cc572..b25670b 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -6,10 +6,15 @@ use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; use rand_core::OsRng; use reqwest::Client; use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Manager, State}; +use tauri::{ + menu::{MenuBuilder, MenuItemBuilder}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Manager, State, WebviewWindow, WindowEvent, +}; use x25519_dalek::{PublicKey, StaticSecret}; const PROFILE_NAME: &str = "NexaVPN"; +const MAIN_WINDOW_LABEL: &str = "main"; struct AppState { session: Mutex>, @@ -263,12 +268,73 @@ fn ensure_app_dir(app: &AppHandle) -> Result { Ok(dir) } +fn restore_main_window(window: &WebviewWindow) { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); +} + +fn hide_main_window(window: &WebviewWindow) { + let _ = window.hide(); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .setup(|app| { + let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?; + let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?; + let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?; + + let mut tray = TrayIconBuilder::new().menu(&menu).show_menu_on_left_click(false); + if let Some(icon) = app.default_window_icon() { + tray = tray.icon(icon.clone()); + } + + tray + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + restore_main_window(&window); + } + } + "quit" => { + app.exit(0); + } + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + restore_main_window(&window); + } + } + }) + .build(app)?; + + Ok(()) + }) .manage(AppState { session: Mutex::new(None), }) + .on_window_event(|window, event| match event { + WindowEvent::CloseRequested { api, .. } => { + api.prevent_close(); + hide_main_window(window); + } + WindowEvent::Resized(_) => { + if window.is_minimized().unwrap_or(false) { + hide_main_window(window); + } + } + _ => {} + }) .invoke_handler(tauri::generate_handler![load_state, enroll_device, connect_tunnel, disconnect_tunnel]) .run(tauri::generate_context!()) .expect("error while running tauri application");