diff --git a/README.md b/README.md index 3ab31bd..257ee16 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,14 @@ Planned: Transmission v2.40 or later is required. +## Startup visibility + +In **Application Settings > Integrations**, enable **Show tray icon** and +**Hide on startup** to launch with only a tray icon. The option is off by default +and takes effect after restarting the application. If a tray icon cannot be +created, the window opens normally. Use the tray icon or launch the application +again to show the existing window, including when no server tab is open. + ## Downloads You can get the latest release from the diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 74b1ebb..caa291d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -283,6 +283,30 @@ pub async fn list_system_fonts() -> Vec { system_fonts::query_all() } +#[tauri::command] +pub fn finish_startup( + app: tauri::AppHandle, + window: tauri::WebviewWindow, + startup: State<'_, crate::StartupState>, + hide_on_startup: bool, +) -> Result<(), String> { + if window.label() != "main" { + return Err("Startup visibility is only available for the main window".into()); + } + let first_window = startup.0.swap(false, std::sync::atomic::Ordering::SeqCst); + let hidden = first_window && hide_on_startup && app.tray_by_id(tray::TRAY_ID).is_some(); + if hidden { + window.hide().map_err(|e| e.to_string())?; + tray::set_tray_showhide_text(&app, "Show"); + } else { + window.show().map_err(|e| e.to_string())?; + window.set_focus().ok(); + tray::set_tray_showhide_text(&app, "Hide"); + } + println!("Main window startup: {}", if hidden { "hidden" } else { "visible" }); + Ok(()) +} + #[tauri::command] pub fn create_tray(app_handle: tauri::AppHandle) { if app_handle.tray_by_id(tray::TRAY_ID).is_none() { diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index d18d0f1..f792dd9 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -137,6 +137,19 @@ async fn http_response( toggle_main_window(&app, None); } + // Opening another instance without files must also work when no + // server tab is open (the frontend argument listener is not mounted). + if serde_json::from_slice::>(&payload).is_ok_and(|args| args.is_empty()) { + if let Some(window) = app.get_webview_window("main") { + window.show().ok(); + window.unminimize().ok(); + window.set_focus().ok(); + window.emit("window-shown", "").ok(); + crate::tray::set_tray_showhide_text(&app, "Hide"); + } + return Ok(Response::builder().body(make_body("TrguiNG OK")).unwrap()); + } + let lock = args_lock.acquire().await.unwrap(); send_payload(&app, payload).await; drop(lock); @@ -453,6 +466,11 @@ impl Ipc { } pub async fn send(&self, args: &Vec, app: AppHandle) -> Result<(), String> { + // Only a second instance or real launch arguments should reveal the UI. + // The primary instance already creates its window during setup. + if self.listening && args.is_empty() { + return Ok(()); + } let client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(3)) .read_timeout(Duration::from_secs(10)) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b4ce60f..18d4a48 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -19,7 +19,7 @@ windows_subsystem = "windows" )] -use std::{sync::Arc, time::Duration}; +use std::{sync::{Arc, atomic::AtomicBool}, time::Duration}; use createtorrent::CreationRequestsHandle; use geoip::MmdbReaderHandle; @@ -42,6 +42,7 @@ mod torrentcache; mod tray; struct ListenerHandle(Arc>); +struct StartupState(AtomicBool); #[cfg(target_os = "macos")] fn handle_uris(app: &AppHandle, uris: Vec) { @@ -208,10 +209,12 @@ fn main() { commands::save_create_torrent, commands::pass_to_window, commands::list_system_fonts, + commands::finish_startup, commands::create_tray, commands::save_text_file, commands::load_text_file, ]) + .manage(StartupState(AtomicBool::new(true))) .manage(ListenerHandle(Arc::new(RwLock::new(ipc)))) .manage(TorrentCacheHandle::default()) .manage(PollerHandle::default()) diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 3a5ed9b..91eb9fe 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -110,14 +110,13 @@ pub fn toggle_main_window(app: &AppHandle, window: Option) { }); } None => { - let window = - WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) + WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("Transmission GUI") .inner_size(1024., 800.) + .visible(false) .build() .unwrap(); set_tray_showhide_text(app, "Hide"); - window.set_focus().ok(); } } } diff --git a/src/components/modals/settings.tsx b/src/components/modals/settings.tsx index 9bd6c02..ecfdc60 100644 --- a/src/components/modals/settings.tsx +++ b/src/components/modals/settings.tsx @@ -42,6 +42,7 @@ interface FormValues extends InterfaceFormValues { toastNotifications: boolean, toastNotificationSound: boolean, showTrayIcon: boolean, + hideOnStartup: boolean, onMinimize: WindowMinimizeOption, onClose: WindowCloseOption, }, @@ -303,6 +304,7 @@ function IntegrationsPanel({ form }: { form: UseFormReturnType }) { onChange={(e) => { form.setFieldValue("app.showTrayIcon", e.currentTarget.checked); if (!e.currentTarget.checked) { + form.setFieldValue("app.hideOnStartup", false); form.setFieldValue("app.onMinimize", "minimize"); form.setFieldValue("app.onClose", "quit"); } @@ -310,6 +312,18 @@ function IntegrationsPanel({ form }: { form: UseFormReturnType }) { /> (takes effect after restart) + Hide on startup + + + + Start in the system tray (after restart) When minimized