diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index b74c7d65c..008fc0721 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -3,6 +3,7 @@ mod services; +use services::settings::{CLOSE_TO_TRAY_KEY, START_MINIMIZED_KEY, STORE_PATH}; use sysinfo::System; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::path::BaseDirectory; @@ -13,9 +14,6 @@ use tauri_plugin_autostart::ManagerExt; use tauri_plugin_shell::ShellExt; use tauri_plugin_store::StoreExt; -const STORE_PATH: &str = "settings.json"; -const CLOSE_TO_TRAY_KEY: &str = "close_to_tray"; - const ENDPOINTS: [(&str, &str, &str); 2] = [ ( "BACKEND", @@ -258,22 +256,6 @@ fn is_autostart_enabled(app: tauri::AppHandle) -> Result { app.autolaunch().is_enabled().map_err(|e| e.to_string()) } -#[tauri::command] -fn get_close_to_tray(app: tauri::AppHandle) -> Result { - let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; - Ok(store - .get(CLOSE_TO_TRAY_KEY) - .and_then(|v| v.as_bool()) - .unwrap_or(true)) -} - -#[tauri::command] -fn set_close_to_tray(app: tauri::AppHandle, enabled: bool) -> Result<(), String> { - let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; - store.set(CLOSE_TO_TRAY_KEY, enabled); - store.save().map_err(|e| e.to_string()) -} - fn main() { tauri::Builder::default() // Auto-start: pass --minimized so the window starts hidden when launched at boot @@ -296,9 +278,19 @@ fn main() { prod(app.handle(), &resource_path)?; // When auto-started at boot (--minimized flag), keep the window hidden + // unless the user has opted out via the "Start Minimized" preference. if std::env::args().any(|a| a == "--minimized") { - if let Some(window) = app.get_webview_window("main") { - let _ = window.hide(); + let start_minimized = app + .store(STORE_PATH) + .ok() + .and_then(|s| s.get(START_MINIMIZED_KEY)) + .and_then(|v| v.as_bool()) + .unwrap_or(true); // default: start minimized to tray + + if start_minimized { + if let Some(window) = app.get_webview_window("main") { + let _ = window.hide(); + } } } @@ -359,8 +351,10 @@ fn main() { enable_autostart, disable_autostart, is_autostart_enabled, - get_close_to_tray, - set_close_to_tray, + services::settings::get_close_to_tray, + services::settings::set_close_to_tray, + services::settings::get_start_minimized, + services::settings::set_start_minimized, ]) .on_window_event(on_window_event) .build(tauri::generate_context!()) diff --git a/frontend/src-tauri/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index a8948ab45..8b6f2356e 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -1,3 +1,4 @@ +pub mod settings; pub mod tunnel; use tauri::path::BaseDirectory; diff --git a/frontend/src-tauri/src/services/settings.rs b/frontend/src-tauri/src/services/settings.rs new file mode 100644 index 000000000..79991badc --- /dev/null +++ b/frontend/src-tauri/src/services/settings.rs @@ -0,0 +1,37 @@ +use tauri_plugin_store::StoreExt; + +pub const STORE_PATH: &str = "settings.json"; +pub const CLOSE_TO_TRAY_KEY: &str = "close_to_tray"; +pub const START_MINIMIZED_KEY: &str = "start_minimized"; + +#[tauri::command] +pub fn get_close_to_tray(app: tauri::AppHandle) -> Result { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + Ok(store + .get(CLOSE_TO_TRAY_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(true)) +} + +#[tauri::command] +pub fn set_close_to_tray(app: tauri::AppHandle, enabled: bool) -> Result<(), String> { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + store.set(CLOSE_TO_TRAY_KEY, enabled); + store.save().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn get_start_minimized(app: tauri::AppHandle) -> Result { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + Ok(store + .get(START_MINIMIZED_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(true)) // default: start minimized to tray, matching prior behavior +} + +#[tauri::command] +pub fn set_start_minimized(app: tauri::AppHandle, enabled: bool) -> Result<(), String> { + let store = app.store(STORE_PATH).map_err(|e| e.to_string())?; + store.set(START_MINIMIZED_KEY, enabled); + store.save().map_err(|e| e.to_string()) +} diff --git a/frontend/src/pages/SettingsPage/components/SettingSwitchRow.tsx b/frontend/src/pages/SettingsPage/components/SettingSwitchRow.tsx new file mode 100644 index 000000000..50a023b4d --- /dev/null +++ b/frontend/src/pages/SettingsPage/components/SettingSwitchRow.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +interface SettingSwitchRowProps { + /** + * Base id used to derive the label/description ids referenced by + * aria-labelledby / aria-describedby + */ + id: string; + /** + * Row title + */ + label: string; + /** + * Row description + */ + description: string; + /** + * Whether the switch is on + */ + checked: boolean; + /** + * Whether the switch is disabled (loading / pending / unknown state) + */ + disabled: boolean; + /** + * Called when the switch is toggled + */ + onToggle: () => void; +} + +/** + * Reusable labeled toggle switch row used by the System settings card + */ +const SettingSwitchRow: React.FC = ({ + id, + label, + description, + checked, + disabled, + onToggle, +}) => { + const labelId = `${id}-label`; + const descId = `${id}-desc`; + + return ( +
+
+
+ {label} +
+
+ {description} +
+
+ + +
+ ); +}; + +export default SettingSwitchRow; diff --git a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx index 4a05be707..d8a2fb859 100644 --- a/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx +++ b/frontend/src/pages/SettingsPage/components/SystemSettingsCard.tsx @@ -2,14 +2,17 @@ import React, { useEffect, useState } from 'react'; import { Monitor } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import SettingsCard from './SettingsCard'; +import SettingSwitchRow from './SettingSwitchRow'; const SystemSettingsCard: React.FC = () => { // null = unknown / error reading state const [autostart, setAutostart] = useState(null); const [closeToTray, setCloseToTray] = useState(null); + const [startMinimized, setStartMinimized] = useState(null); const [loading, setLoading] = useState(true); const [pending, setPending] = useState(false); const [pendingCloseToTray, setPendingCloseToTray] = useState(false); + const [pendingStartMinimized, setPendingStartMinimized] = useState(false); useEffect(() => { Promise.all([ @@ -19,6 +22,9 @@ const SystemSettingsCard: React.FC = () => { invoke('get_close_to_tray') .then(setCloseToTray) .catch(() => setCloseToTray(null)), + invoke('get_start_minimized') + .then(setStartMinimized) + .catch(() => setStartMinimized(null)), ]).finally(() => setLoading(false)); }, []); @@ -50,6 +56,20 @@ const SystemSettingsCard: React.FC = () => { } }; + const handleStartMinimizedToggle = async () => { + if (startMinimized === null) return; + const next = !startMinimized; + setPendingStartMinimized(true); + try { + await invoke('set_start_minimized', { enabled: next }); + setStartMinimized(next); + } catch (err) { + console.error('Failed to toggle start minimized:', err); + } finally { + setPendingStartMinimized(false); + } + }; + const isDisabled = loading || pending || autostart === null; const isChecked = autostart === true; @@ -57,90 +77,49 @@ const SystemSettingsCard: React.FC = () => { loading || pendingCloseToTray || closeToTray === null; const closeToTrayChecked = closeToTray === true; + const startMinimizedDisabled = + loading || pendingStartMinimized || startMinimized === null; + const startMinimizedChecked = startMinimized === true; + + const autostartDescription = + startMinimized === false + ? 'Automatically start PictoPy when you log in. The main window opens directly on boot.' + : 'Automatically start PictoPy when you log in. The window starts minimized to the system tray.'; + return ( -
-
-
- Launch at startup -
-
- Automatically start PictoPy when you log in. The window starts - minimized to the system tray. -
-
- - -
+ -
-
-
- Close to tray -
-
- When enabled, closing the window hides the app to the system tray - instead of exiting. -
-
+ {isChecked && ( + + )} - -
+
); };