From a9016c85c6f2516263c05fdad4d99dfa1b36ff17 Mon Sep 17 00:00:00 2001 From: Stefano Novelli Date: Fri, 22 Aug 2025 12:05:32 +0200 Subject: [PATCH] feat: add 'Hide Icon on Close' setting and related functionality --- src-tauri/Cargo.lock | 56 +++++++++++++++++++ src-tauri/Cargo.toml | 2 + src-tauri/src/lib.rs | 92 +++++++++++++++++++++++++++++++- src/index.html | 9 ++++ src/managers/settings-manager.js | 65 +++++++++++++++++++++- src/utils/theme-loader.js | 2 +- 6 files changed, 222 insertions(+), 4 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7e905d1..6290e50 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -301,6 +301,12 @@ dependencies = [ "serde", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -517,6 +523,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "combine" version = "4.6.7" @@ -2185,6 +2221,15 @@ dependencies = [ "time", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2408,6 +2453,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3053,10 +3107,12 @@ version = "0.3.10" dependencies = [ "base64 0.21.7", "chrono", + "cocoa", "core-foundation 0.9.4", "core-graphics 0.23.2", "dotenv", "libc", + "objc", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d58cba0..bbed9d3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -51,4 +51,6 @@ base64 = "0.21" core-graphics = "0.23" core-foundation = "0.9" libc = "0.2" +cocoa = "0.25" +objc = "0.2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8481916..5ea6b19 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -87,6 +87,8 @@ struct AppSettings { autostart: bool, #[serde(default = "default_analytics_enabled")] analytics_enabled: bool, + #[serde(default)] + hide_icon_on_close: bool, } #[derive(Serialize, Deserialize, Clone)] @@ -174,6 +176,7 @@ impl Default for AppSettings { advanced: AdvancedSettings::default(), autostart: false, // default to disabled analytics_enabled: true, // default to enabled + hide_icon_on_close: false, // default to disabled } } } @@ -612,6 +615,22 @@ async fn update_tray_icon( #[tauri::command] async fn show_window(app: AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("main") { + // Check if hide_icon_on_close is enabled to restore dock visibility + match load_settings(app.clone()).await { + Ok(settings) => { + if settings.hide_icon_on_close { + // Restore dock visibility when showing window + #[cfg(target_os = "macos")] + { + let _ = set_dock_visibility(app.clone(), true).await; + } + } + } + Err(_) => { + // Ignore error, just proceed with showing window + } + } + window .show() .map_err(|e| format!("Failed to show window: {}", e))?; @@ -929,7 +948,8 @@ pub fn run() { save_session_tags, add_session_tag, write_excel_file, - start_oauth_server + start_oauth_server, + set_dock_visibility ]) .setup(|app| { // Track app started event (if enabled) @@ -1023,9 +1043,42 @@ pub fn run() { .build(app)?; if let Some(window) = app.get_webview_window("main") { + let app_handle_for_close = app.handle().clone(); window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Always prevent close api.prevent_close(); + + // Check if we should hide the app icon + let app_handle_clone = app_handle_for_close.clone(); + tauri::async_runtime::spawn(async move { + match load_settings(app_handle_clone.clone()).await { + Ok(settings) => { + if settings.hide_icon_on_close { + // Hide the window and set app as dock hidden + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + // Use macOS specific API to hide from dock + #[cfg(target_os = "macos")] + { + let _ = set_dock_visibility(app_handle_clone.clone(), false).await; + } + } + } else { + // Just hide the window without hiding from dock + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + } + } + } + Err(_) => { + // Default behavior: just hide the window + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + } + } + } + }); } }); } @@ -1283,3 +1336,40 @@ async fn start_oauth_server(window: tauri::Window) -> Result { }) .map_err(|err| err.to_string()) } + +#[tauri::command] +async fn set_dock_visibility(app: AppHandle, visible: bool) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + app.run_on_main_thread(move || { + set_dock_visibility_native(visible); + }) + .map_err(|e| format!("Failed to run on main thread: {}", e))?; + } + + #[cfg(not(target_os = "macos"))] + { + return Err("Dock visibility is only supported on macOS".to_string()); + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn set_dock_visibility_native(visible: bool) { + use cocoa::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy}; + use cocoa::base::nil; + + unsafe { + let app = NSApp(); + if app != nil { + let policy = if visible { + NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular + } else { + NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory + }; + + app.setActivationPolicy_(policy); + } + } +} diff --git a/src/index.html b/src/index.html index 23a8dd2..de838bb 100644 --- a/src/index.html +++ b/src/index.html @@ -797,6 +797,15 @@

System Integration

start minimized to the system tray.

+ +
+ +

Hide the app icon from the dock when closing the window with X. The app will continue running in the system tray.

+
diff --git a/src/managers/settings-manager.js b/src/managers/settings-manager.js index c2c4444..5098782 100644 --- a/src/managers/settings-manager.js +++ b/src/managers/settings-manager.js @@ -83,7 +83,8 @@ export class SettingsManager { appearance: { ...defaultSettings.appearance, ...loadedSettings.appearance }, advanced: { ...defaultSettings.advanced, ...loadedSettings.advanced }, autostart: loadedSettings.autostart !== undefined ? loadedSettings.autostart : defaultSettings.autostart, - analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled + analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled, + hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close }; } @@ -118,7 +119,8 @@ export class SettingsManager { debug_mode: false // Debug mode with 3-second timers }, autostart: false, // default to disabled - analytics_enabled: true // Analytics enabled by default + analytics_enabled: true, // Analytics enabled by default + hide_icon_on_close: false // Hide icon on close disabled by default }; } @@ -198,6 +200,9 @@ export class SettingsManager { // Populate analytics setting this.loadAnalyticsSetting(); + + // Populate hide icon on close setting + this.loadHideIconOnCloseSetting(); } setupEventListeners() { @@ -860,6 +865,62 @@ export class SettingsManager { } } + async loadHideIconOnCloseSetting() { + try { + // Get current hide icon on close setting from our stored settings + const hideIconOnClose = this.settings.hide_icon_on_close; + + const checkbox = document.getElementById('hide-icon-on-close'); + if (checkbox) { + checkbox.checked = hideIconOnClose; + + // Setup event listener for the hide icon on close checkbox + checkbox.addEventListener('change', async (e) => { + await this.toggleHideIconOnClose(e.target.checked); + }); + } + } catch (error) { + console.error('Failed to load hide icon on close setting:', error); + // Default to disabled if we can't check the status + const checkbox = document.getElementById('hide-icon-on-close'); + if (checkbox) { + checkbox.checked = false; + checkbox.addEventListener('change', async (e) => { + await this.toggleHideIconOnClose(e.target.checked); + }); + } + } + } + + async toggleHideIconOnClose(enabled) { + try { + // Update our settings + this.settings.hide_icon_on_close = enabled; + + // Show user feedback + if (enabled) { + console.log('Hide icon on close enabled'); + NotificationUtils.showNotificationPing('✓ Hide icon on close enabled - App will hide from dock when closed', 'success'); + } else { + console.log('Hide icon on close disabled'); + NotificationUtils.showNotificationPing('✓ Hide icon on close disabled - App will remain visible in dock', 'success'); + } + + // Schedule auto-save to persist the setting + this.scheduleAutoSave(); + + } catch (error) { + console.error('Failed to toggle hide icon on close:', error); + NotificationUtils.showNotificationPing('❌ Failed to toggle hide icon on close: ' + error, 'error'); + + // Revert the checkbox state on error + const checkbox = document.getElementById('hide-icon-on-close'); + if (checkbox) { + checkbox.checked = !enabled; + } + } + } + // Theme management functions async applyTheme(theme) { const html = document.documentElement; diff --git a/src/utils/theme-loader.js b/src/utils/theme-loader.js index 14e3dfa..41d1a3f 100644 --- a/src/utils/theme-loader.js +++ b/src/utils/theme-loader.js @@ -39,7 +39,7 @@ class ThemeLoader { // that gets updated by the build process or manually maintained // This could be enhanced to use a build-time script that generates this list - const knownThemes = [ + const knownThemes = [ 'espresso.css', 'pipboy.css', 'pommodore64.css'