diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10a6b90b..0fd178bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The **Server** lyrics source now highlights lyrics word by word, so karaoke sync no longer depends on the third-party YouLyPlus backend. Requires Navidrome 0.63 or newer and lyrics that carry word timing (TTML or Enhanced LRC); anything else keeps highlighting line by line.
* **Settings → Lyrics → Lyrics Sources** spells out those requirements, and the block now follows the standard settings sub-card layout.
+### Start minimized to tray
+
+**By [@cucadmuh](https://github.com/cucadmuh), PR [#1271](https://github.com/Psychotoxical/psysonic/pull/1271)**
+
+* New **Start Minimized to Tray** toggle under **Settings → System → Behavior**. When enabled, the next cold start keeps the main window hidden and Psysonic runs from the system tray until you show it from the tray icon.
+* Requires **Show Tray Icon** (turning this on enables the tray automatically; hiding the tray clears the setting). The choice applies on the next launch only — toggling it in Settings does not hide the window immediately.
+
## Changed
diff --git a/public/startup-splash-preflight.js b/public/startup-splash-preflight.js
index 34099c32..4a30b9a1 100644
--- a/public/startup-splash-preflight.js
+++ b/public/startup-splash-preflight.js
@@ -155,7 +155,25 @@
}
var persisted = readThemeState();
+ function readStartMinimizedToTray() {
+ try {
+ var raw = localStorage.getItem('psysonic-auth');
+ if (!raw) return false;
+ var parsed = JSON.parse(raw);
+ var state = parsed && parsed.state;
+ if (!state || !state.startMinimizedToTray) return false;
+ return state.showTrayIcon !== false;
+ } catch (_err) {
+ return false;
+ }
+ }
+
var themeId = persisted ? resolveScheduledTheme(persisted) : 'mocha';
var palette = paletteForTheme(themeId, readInstalledThemes());
applyPalette(themeId, palette);
+ var trayHandled = false;
+ try {
+ trayHandled = sessionStorage.getItem('psy-startup-tray-handled') === '1';
+ } catch (_err) {}
+ window.__psyStartMinimizedToTray = readStartMinimizedToTray() && !trayHandled;
})();
diff --git a/public/startup-splash-reveal.js b/public/startup-splash-reveal.js
index 7c3c9513..a4c95bc7 100644
--- a/public/startup-splash-reveal.js
+++ b/public/startup-splash-reveal.js
@@ -13,6 +13,7 @@
}
function reveal(attempt) {
+ if (window.__psyStartMinimizedToTray) return;
if (tryShowMainWindow()) return;
if (attempt >= MAX_ATTEMPTS) return;
window.setTimeout(function () {
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 4c3b78d6..964ae6be 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -895,9 +895,7 @@ pub fn run() {
}
let _ = window.hide();
if let Some(main) = window.app_handle().get_webview_window("main") {
- let _ = main.unminimize();
- let _ = main.show();
- let _ = main.set_focus();
+ let _ = crate::lib_commands::ui::mini::restore_main_window(&main);
}
}
}
@@ -909,14 +907,18 @@ pub fn run() {
match payload.event() {
tauri::webview::PageLoadEvent::Started => {
- let window = webview.window().clone();
+ let app = webview.app_handle().clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(48));
- let _ = window.show();
+ if let Some(window) = app.get_webview_window("main") {
+ crate::lib_commands::ui::mini::eval_startup_main_window_visibility(&window);
+ }
});
}
tauri::webview::PageLoadEvent::Finished => {
- let _ = webview.window().show();
+ if let Some(window) = webview.app_handle().get_webview_window("main") {
+ crate::lib_commands::ui::mini::eval_startup_main_window_visibility(&window);
+ }
}
}
})
diff --git a/src-tauri/src/lib_commands/ui/mini.rs b/src-tauri/src/lib_commands/ui/mini.rs
index 54b6bbeb..602cd470 100644
--- a/src-tauri/src/lib_commands/ui/mini.rs
+++ b/src-tauri/src/lib_commands/ui/mini.rs
@@ -169,6 +169,51 @@ document.documentElement.style.removeProperty('--psy-anim-speed');
})();
"#;
+/// Show the main window after startup splash paint, or pause rendering when the
+/// user chose "start minimized to tray" (flag set in `startup-splash-preflight.js`).
+pub(crate) fn eval_startup_main_window_visibility(window: &tauri::WebviewWindow) {
+ let js = format!(
+ "(function () {{
+ try {{
+ if (sessionStorage.getItem('psy-startup-tray-handled') === '1') return;
+ }} catch (e) {{}}
+ var deferToTray = !!window.__psyStartMinimizedToTray;
+ if (!deferToTray) {{
+ try {{
+ var raw = localStorage.getItem('psysonic-auth');
+ if (raw) {{
+ var state = JSON.parse(raw).state;
+ deferToTray = !!(state && state.startMinimizedToTray && state.showTrayIcon !== false);
+ }}
+ }} catch (e) {{}}
+ }}
+ var internals = window.__TAURI_INTERNALS__;
+ if (deferToTray) {{
+ {pause}
+ try {{ sessionStorage.setItem('psy-startup-tray-handled', '1'); }} catch (e) {{}}
+ if (internals && typeof internals.invoke === 'function') {{
+ internals.invoke('plugin:window|hide', {{ label: 'main' }}).catch(function () {{}});
+ }}
+ return;
+ }}
+ if (internals && typeof internals.invoke === 'function') {{
+ internals.invoke('plugin:window|show', {{ label: 'main' }}).catch(function () {{}});
+ }}
+}})();",
+ pause = PAUSE_RENDERING_JS.trim(),
+ );
+ let _ = window.eval(&js);
+}
+
+/// Resume rendering and bring the main window to the foreground.
+pub(crate) fn restore_main_window(main: &tauri::WebviewWindow) -> Result<(), String> {
+ main.eval(RESUME_RENDERING_JS).map_err(|e| e.to_string())?;
+ main.unminimize().map_err(|e| e.to_string())?;
+ main.show().map_err(|e| e.to_string())?;
+ main.set_focus().map_err(|e| e.to_string())?;
+ Ok(())
+}
+
/// Build the mini player webview window. Caller decides `visible` so the
/// same code path serves both pre-creation (Windows, hidden at app start)
/// and lazy creation (other platforms, shown on demand).
@@ -299,9 +344,7 @@ pub(crate) fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
let _ = win.eval(PAUSE_RENDERING_JS);
win.hide().map_err(|e| e.to_string())?;
if let Some(main) = app.get_webview_window("main") {
- let _ = main.unminimize();
- let _ = main.show();
- let _ = main.set_focus();
+ let _ = restore_main_window(&main);
}
} else {
// Resume rendering before showing — the window needs to be ready
@@ -341,9 +384,7 @@ pub(crate) fn close_mini_player(app: tauri::AppHandle) -> Result<(), String> {
win.hide().map_err(|e| e.to_string())?;
}
if let Some(main) = app.get_webview_window("main") {
- let _ = main.unminimize();
- let _ = main.show();
- let _ = main.set_focus();
+ restore_main_window(&main)?;
}
Ok(())
}
@@ -360,10 +401,7 @@ pub(crate) fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
let _ = mini.hide();
}
if let Some(main) = app.get_webview_window("main") {
- let _ = main.eval(RESUME_RENDERING_JS);
- main.unminimize().map_err(|e| e.to_string())?;
- main.show().map_err(|e| e.to_string())?;
- main.set_focus().map_err(|e| e.to_string())?;
+ restore_main_window(&main)?;
}
Ok(())
}
diff --git a/src/app/startupSplash.test.ts b/src/app/startupSplash.test.ts
index 6f85b2a8..884e94c2 100644
--- a/src/app/startupSplash.test.ts
+++ b/src/app/startupSplash.test.ts
@@ -13,12 +13,18 @@ vi.mock('@/lib/themes/startupThemeAppearance', () => ({
applyStartupSplashThemeFromStorage: vi.fn(() => 'mocha'),
}));
+vi.mock('@/lib/settings/readStartMinimizedToTray', () => ({
+ shouldDeferMainWindowReveal: vi.fn(() => false),
+}));
+
vi.mock('@tauri-apps/api/webviewWindow', () => ({
getCurrentWebviewWindow: vi.fn(() => ({ show: vi.fn(() => Promise.resolve()) })),
}));
import { getWindowKind } from './windowKind';
import { applyStartupSplashThemeFromStorage } from '@/lib/themes/startupThemeAppearance';
+import { shouldDeferMainWindowReveal } from '@/lib/settings/readStartMinimizedToTray';
+import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
describe('startupSplash', () => {
beforeEach(() => {
@@ -42,6 +48,14 @@ describe('startupSplash', () => {
expect(applyStartupSplashThemeFromStorage).toHaveBeenCalled();
});
+ it('skips reveal when start minimized to tray is enabled', () => {
+ vi.mocked(shouldDeferMainWindowReveal).mockReturnValue(true);
+ const show = vi.fn(() => Promise.resolve());
+ vi.mocked(getCurrentWebviewWindow).mockReturnValue({ show } as never);
+ configureStartupSplash();
+ expect(show).not.toHaveBeenCalled();
+ });
+
it('fades out and removes splash', () => {
vi.useFakeTimers();
dismissStartupSplash();
diff --git a/src/app/startupSplash.ts b/src/app/startupSplash.ts
index 592319db..3ac8df13 100644
--- a/src/app/startupSplash.ts
+++ b/src/app/startupSplash.ts
@@ -1,4 +1,5 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
+import { shouldDeferMainWindowReveal } from '@/lib/settings/readStartMinimizedToTray';
import { applyStartupSplashThemeFromStorage } from '@/lib/themes/startupThemeAppearance';
import { getWindowKind } from './windowKind';
@@ -7,6 +8,7 @@ export const STARTUP_SPLASH_ID = 'app-startup-splash';
/** Ensure the native shell is visible once the webview bundle is alive. */
export function revealStartupWindow(): void {
if (getWindowKind() === 'mini') return;
+ if (shouldDeferMainWindowReveal()) return;
void getCurrentWebviewWindow().show().catch(() => {});
}
diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts
index e2b0b309..6077780a 100644
--- a/src/config/settingsCredits.ts
+++ b/src/config/settingsCredits.ts
@@ -185,6 +185,7 @@ const CONTRIBUTOR_ENTRIES = [
'Queue — resolve thin-state rows off the visible range so off-window items stop rendering as "…" placeholders (desktop panel, mobile drawer, fullscreen up-next) (PR #1236)',
'Artists browse — case-insensitive Cyrillic/non-ASCII name search when local index is enabled (PR #1237)',
'CLI — relative volume via signed `volume` argument (+/− percent delta); suppress WebKit NVIDIA stderr notes on CLI argv; faster Linux CLI forward before WebKit init (PR #1238)',
+ 'Settings — start minimized to tray on cold launch; session-gated startup hide; tray icon coupling (PR #1271)',
'Multi-library filter — priority-ordered multi-select scope across browse/search/detail, sargable library_id + FTS-first SQL, rebuildable library-cluster.db identity keys, locale-aware name normalization (PR #1241)',
'Genres — full catalog via indexed SQL when All libraries is selected; no longer samples first album page on large libraries (PR #1242)',
'Offline browse — on-disk-only Artists/Albums/Tracks/Genres (pins, favorites-auto, hot-cache); reactive sidebar gates and sync-idle reload; local credit mode and genre scope (PR #1243)',
diff --git a/src/features/settings/components/SystemTab.tsx b/src/features/settings/components/SystemTab.tsx
index eccdfe73..0497a58a 100644
--- a/src/features/settings/components/SystemTab.tsx
+++ b/src/features/settings/components/SystemTab.tsx
@@ -161,6 +161,18 @@ export function SystemTab() {
checked={auth.minimizeToTray}
onChange={auth.setMinimizeToTray}
/>
+
+
{IS_LINUX && (
diff --git a/src/features/settings/components/settingsTabs.ts b/src/features/settings/components/settingsTabs.ts
index 8b646fc2..16730c4c 100644
--- a/src/features/settings/components/settingsTabs.ts
+++ b/src/features/settings/components/settingsTabs.ts
@@ -71,7 +71,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'input', titleKey: 'settings.inputKeybindingsTitle', keywords: 'keybindings shortcuts hotkeys keyboard' },
{ tab: 'input', titleKey: 'settings.globalShortcutsTitle', keywords: 'global shortcuts hotkeys system-wide media keys' },
{ tab: 'system', titleKey: 'settings.language', keywords: 'language locale translation i18n' },
- { tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start smooth scroll linux' },
+ { tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start minimized launch hidden smooth scroll linux' },
{ tab: 'system', titleKey: 'settings.backupTitle', keywords: 'backup export import settings restore' },
{ tab: 'system', titleKey: 'settings.loggingTitle', keywords: 'log logs diagnostic debug verbose' },
{ tab: 'system', titleKey: 'settings.aboutTitle', keywords: 'about version update changelog release notes' },
diff --git a/src/lib/settings/readStartMinimizedToTray.test.ts b/src/lib/settings/readStartMinimizedToTray.test.ts
new file mode 100644
index 00000000..a1ecc599
--- /dev/null
+++ b/src/lib/settings/readStartMinimizedToTray.test.ts
@@ -0,0 +1,64 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import {
+ STARTUP_TRAY_HANDLED_KEY,
+ isStartupTrayHandledThisSession,
+ markStartupTrayHandledThisSession,
+ readStartMinimizedToTray,
+ shouldDeferMainWindowReveal,
+ shouldDeferMainWindowRevealThisSession,
+} from './readStartMinimizedToTray';
+
+describe('readStartMinimizedToTray', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ delete window.__psyStartMinimizedToTray;
+ });
+
+ it('returns false when unset', () => {
+ expect(readStartMinimizedToTray()).toBe(false);
+ });
+
+ it('reads persisted value from psysonic-auth', () => {
+ localStorage.setItem(
+ 'psysonic-auth',
+ JSON.stringify({ state: { startMinimizedToTray: true } }),
+ );
+ expect(readStartMinimizedToTray()).toBe(true);
+ });
+
+ it('returns false when tray icon is disabled', () => {
+ localStorage.setItem(
+ 'psysonic-auth',
+ JSON.stringify({ state: { startMinimizedToTray: true, showTrayIcon: false } }),
+ );
+ expect(readStartMinimizedToTray()).toBe(false);
+ });
+});
+
+describe('startup tray session gate', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ delete window.__psyStartMinimizedToTray;
+ });
+
+ it('defers reveal only on the first load when the setting is on', () => {
+ localStorage.setItem(
+ 'psysonic-auth',
+ JSON.stringify({ state: { startMinimizedToTray: true } }),
+ );
+ expect(shouldDeferMainWindowRevealThisSession()).toBe(true);
+ markStartupTrayHandledThisSession();
+ expect(shouldDeferMainWindowRevealThisSession()).toBe(false);
+ expect(isStartupTrayHandledThisSession()).toBe(true);
+ expect(sessionStorage.getItem(STARTUP_TRAY_HANDLED_KEY)).toBe('1');
+ });
+
+ it('prefers the preflight flag when present', () => {
+ window.__psyStartMinimizedToTray = true;
+ expect(shouldDeferMainWindowReveal()).toBe(true);
+ window.__psyStartMinimizedToTray = false;
+ expect(shouldDeferMainWindowReveal()).toBe(false);
+ });
+});
diff --git a/src/lib/settings/readStartMinimizedToTray.ts b/src/lib/settings/readStartMinimizedToTray.ts
new file mode 100644
index 00000000..09ba3197
--- /dev/null
+++ b/src/lib/settings/readStartMinimizedToTray.ts
@@ -0,0 +1,51 @@
+const AUTH_STORAGE_KEY = 'psysonic-auth';
+
+/** Keep in sync with `public/startup-splash-preflight.js` and Rust `eval_startup_main_window_visibility`. */
+export const STARTUP_TRAY_HANDLED_KEY = 'psy-startup-tray-handled';
+
+/** Read persisted "start minimized to tray" before Zustand rehydrates. */
+export function readStartMinimizedToTray(): boolean {
+ try {
+ const raw = localStorage.getItem(AUTH_STORAGE_KEY);
+ if (!raw) return false;
+ const parsed = JSON.parse(raw) as {
+ state?: { startMinimizedToTray?: boolean; showTrayIcon?: boolean };
+ };
+ const state = parsed.state;
+ if (!state?.startMinimizedToTray) return false;
+ // Tray icon must be available to restore the window after a hidden start.
+ return state.showTrayIcon !== false;
+ } catch {
+ return false;
+ }
+}
+
+/** Whether startup-to-tray already ran (or was skipped) this app process session. */
+export function isStartupTrayHandledThisSession(): boolean {
+ try {
+ return sessionStorage.getItem(STARTUP_TRAY_HANDLED_KEY) === '1';
+ } catch {
+ return false;
+ }
+}
+
+/** True only on the first document load of a process when the setting is on. */
+export function shouldDeferMainWindowRevealThisSession(): boolean {
+ return readStartMinimizedToTray() && !isStartupTrayHandledThisSession();
+}
+
+/** Prefer the preflight flag when set; otherwise compute from storage + session. */
+export function shouldDeferMainWindowReveal(): boolean {
+ if (typeof window.__psyStartMinimizedToTray === 'boolean') {
+ return window.__psyStartMinimizedToTray;
+ }
+ return shouldDeferMainWindowRevealThisSession();
+}
+
+export function markStartupTrayHandledThisSession(): void {
+ try {
+ sessionStorage.setItem(STARTUP_TRAY_HANDLED_KEY, '1');
+ } catch {
+ // Non-fatal — worst case we re-apply pause once on reload.
+ }
+}
diff --git a/src/locales/bg/settings.ts b/src/locales/bg/settings.ts
index 1f042a60..696f1b9a 100644
--- a/src/locales/bg/settings.ts
+++ b/src/locales/bg/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'Показва иконата на Psysonic в областта за системни известия / менюто.',
minimizeToTray: 'Минимизирай в системната лента',
minimizeToTrayDesc: 'При затваряне на прозореца, запази Psysonic да работи в системната лента, вместо да излиза.',
+ startMinimizedToTray: 'Стартирай минимизиран в трея',
+ startMinimizedToTrayDesc: 'Стартирай Psysonic със скрит главен прозорец; покажи го чрез иконата в трея.',
+ startMinimizedToTrayRequiresTray: 'Първо включи „Показвай икона в системната лента“ — без трей не можеш да отвориш прозореца след скрит старт.',
clockFormat: 'Формат на часовника',
clockFormatDesc: 'Показване на часа, използвано от прогнозното време на опашката и предварителния преглед на таймера за заспиване.',
clockFormatAuto: 'Автоматично (системен регион)',
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts
index e3752ef5..26b3bc03 100644
--- a/src/locales/de/settings.ts
+++ b/src/locales/de/settings.ts
@@ -252,6 +252,9 @@ export const settings = {
showTrayIconDesc: 'Psysonic-Icon im System-Tray / in der Menüleiste anzeigen.',
minimizeToTray: 'Im Tray minimieren',
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
+ startMinimizedToTray: 'Im Tray starten',
+ startMinimizedToTrayDesc: 'Psysonic mit verstecktem Hauptfenster starten; über das Tray-Icon wieder anzeigen.',
+ startMinimizedToTrayRequiresTray: 'Zuerst „Tray-Icon anzeigen“ aktivieren — ohne Tray lässt sich das Fenster nach verstecktem Start nicht öffnen.',
clockFormat: 'Uhrzeitformat',
clockFormatDesc: 'Format für die Queue-ETA und die Vorschau des Sleep-Timers.',
clockFormatAuto: 'Automatisch (Systemsprache)',
diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts
index 2d6ef34f..37976c0c 100644
--- a/src/locales/en/settings.ts
+++ b/src/locales/en/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'Display the Psysonic icon in the system notification area / menu bar.',
minimizeToTray: 'Minimize to Tray',
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
+ startMinimizedToTray: 'Start Minimized to Tray',
+ startMinimizedToTrayDesc: 'Launch Psysonic with the main window hidden; use the tray icon to show it.',
+ startMinimizedToTrayRequiresTray: 'Enable "Show Tray Icon" first — you need the tray to open the window after a hidden start.',
clockFormat: 'Clock Format',
clockFormatDesc: 'Wall-clock display used by the queue ETA and the sleep-timer preview.',
clockFormatAuto: 'Auto (system locale)',
diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts
index 19dc724e..3bf2d8e1 100644
--- a/src/locales/es/settings.ts
+++ b/src/locales/es/settings.ts
@@ -251,6 +251,9 @@ export const settings = {
showTrayIconDesc: 'Muestra el icono de Psysonic en el área de notificación / barra de menú.',
minimizeToTray: 'Minimizar a Bandeja',
minimizeToTrayDesc: 'Al cerrar la ventana, mantener Psysonic ejecutándose en la bandeja del sistema en lugar de salir.',
+ startMinimizedToTray: 'Iniciar minimizado en la bandeja',
+ startMinimizedToTrayDesc: 'Iniciar Psysonic con la ventana principal oculta; usa el icono de la bandeja para mostrarla.',
+ startMinimizedToTrayRequiresTray: 'Activa primero «Mostrar icono en bandeja»; sin él no podrás abrir la ventana tras un inicio oculto.',
clockFormat: 'Formato de hora',
clockFormatDesc: 'Formato del reloj utilizado por la ETA de la cola y la vista previa del temporizador de suspensión.',
clockFormatAuto: 'Automático (idioma del sistema)',
diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts
index ce6dd066..f41b7a38 100644
--- a/src/locales/fr/settings.ts
+++ b/src/locales/fr/settings.ts
@@ -251,6 +251,9 @@ export const settings = {
showTrayIconDesc: 'Affiche l\'icône Psysonic dans la zone de notification / barre des menus.',
minimizeToTray: 'Réduire dans la barre système',
minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.',
+ startMinimizedToTray: 'Démarrer réduit dans la barre système',
+ startMinimizedToTrayDesc: 'Lance Psysonic avec la fenêtre principale masquée ; utilise l\'icône de la barre système pour l\'afficher.',
+ startMinimizedToTrayRequiresTray: 'Activez d\'abord « Afficher l\'icône dans la barre système » — sans elle, la fenêtre ne s\'ouvre pas après un démarrage masqué.',
clockFormat: 'Format de l\'heure',
clockFormatDesc: 'Format de l\'horloge utilisé par l\'ETA de la file d\'attente et l\'aperçu du minuteur de mise en veille.',
clockFormatAuto: 'Automatique (langue du système)',
diff --git a/src/locales/hu/settings.ts b/src/locales/hu/settings.ts
index 1c189809..834bded7 100644
--- a/src/locales/hu/settings.ts
+++ b/src/locales/hu/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'A Psysonic ikon megjelenítése a rendszer értesítési területén / menüsorban.',
minimizeToTray: 'Kis méretre a tálcára',
minimizeToTrayDesc: 'Az ablak bezárásakor a Psysonic kilépés helyett a rendszer tálcáján fut tovább.',
+ startMinimizedToTray: 'Indítás a tálcán',
+ startMinimizedToTrayDesc: 'A Psysonic rejtett főablakkal indul; a megjelenítéshez használd a tálca ikont.',
+ startMinimizedToTrayRequiresTray: 'Először kapcsold be a „Tálcaikon megjelenítése” beállítást — rejtett indítás után csak így nyitható meg az ablak.',
clockFormat: 'Óraformátum',
clockFormatDesc: 'A sor várható befejezési idejéhez és az alvásidőzítő előnézetéhez használt óramegjelenítés.',
clockFormatAuto: 'Automatikus (rendszer beállítása)',
diff --git a/src/locales/it/settings.ts b/src/locales/it/settings.ts
index 252cb8a4..a8551afc 100644
--- a/src/locales/it/settings.ts
+++ b/src/locales/it/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'Mostra l\'icona di Psysonic nell\'area di notifica di sistema / barra dei menu.',
minimizeToTray: 'Riduci nell\'area di notifica',
minimizeToTrayDesc: 'Alla chiusura della finestra, mantieni Psysonic in esecuzione nell\'area di notifica invece di chiuderlo.',
+ startMinimizedToTray: 'Avvia ridotto nell\'area di notifica',
+ startMinimizedToTrayDesc: 'Avvia Psysonic con la finestra principale nascosta; usa l\'icona nell\'area di notifica per mostrarla.',
+ startMinimizedToTrayRequiresTray: 'Abilita prima «Mostra icona nell\'area di notifica» — senza tray non puoi aprire la finestra dopo un avvio nascosto.',
clockFormat: 'Formato orario',
clockFormatDesc: 'Formato dell\'orario usato nella stima della coda e nell\'anteprima del timer di spegnimento.',
clockFormatAuto: 'Automatico (lingua di sistema)',
diff --git a/src/locales/ja/settings.ts b/src/locales/ja/settings.ts
index d0fcfb94..fe49f992 100644
--- a/src/locales/ja/settings.ts
+++ b/src/locales/ja/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'システム通知領域 / メニューバーに Psysonic アイコンを表示します。',
minimizeToTray: 'トレイへ最小化',
minimizeToTrayDesc: 'ウィンドウを閉じたとき、終了せずにシステムトレイで Psysonic を実行し続けます。',
+ startMinimizedToTray: 'トレイで起動',
+ startMinimizedToTrayDesc: 'メインウィンドウを非表示のまま起動します。表示するにはトレイアイコンを使います。',
+ startMinimizedToTrayRequiresTray: '先に「トレイアイコンを表示」を有効にしてください。非表示起動後はトレイから開けます。',
clockFormat: '時計形式',
clockFormatDesc: 'キューの終了予定時刻とスリープタイマーのプレビューに使う時計表示です。',
clockFormatAuto: '自動 (システムロケール)',
diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts
index ed5f15ff..8388eecd 100644
--- a/src/locales/nb/settings.ts
+++ b/src/locales/nb/settings.ts
@@ -250,6 +250,9 @@ export const settings = {
showOrbitTriggerDesc: 'Knappen i toppen for å starte eller bli med i en delt lytteøkt. Skjul den hvis du ikke bruker Orbit — du kan slå den på igjen her.',
minimizeToTray: 'Minimer til oppgavelinjen',
minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.',
+ startMinimizedToTray: 'Start minimert til oppgavelinjen',
+ startMinimizedToTrayDesc: 'Start Psysonic med hovedvinduet skjult; bruk oppgavelinjeikonet for å vise det.',
+ startMinimizedToTrayRequiresTray: 'Aktiver «Vis systemstatusikon» først — uten oppgavelinjen kan du ikke åpne vinduet etter skjult oppstart.',
clockFormat: 'Klokkeformat',
clockFormatDesc: 'Klokkevisning brukt av kø-ETA og forhåndsvisningen av hviletimeren.',
clockFormatAuto: 'Automatisk (systemspråk)',
diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts
index 10ab9883..b6dffa5e 100644
--- a/src/locales/nl/settings.ts
+++ b/src/locales/nl/settings.ts
@@ -251,6 +251,9 @@ export const settings = {
showTrayIconDesc: 'Toont het Psysonic-pictogram in het systeemvak / de menubalk.',
minimizeToTray: 'Minimaliseren naar systeemvak',
minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.',
+ startMinimizedToTray: 'Start geminimaliseerd in systeemvak',
+ startMinimizedToTrayDesc: 'Start Psysonic met het hoofdvenster verborgen; gebruik het tray-pictogram om het te tonen.',
+ startMinimizedToTrayRequiresTray: 'Schakel eerst „Tray-pictogram weergeven” in — zonder tray kun je het venster na een verborgen start niet openen.',
clockFormat: 'Tijdformaat',
clockFormatDesc: 'Tijdsweergave voor de wachtrij-ETA en het voorbeeld van de slaaptimer.',
clockFormatAuto: 'Automatisch (systeemtaal)',
diff --git a/src/locales/pl/settings.ts b/src/locales/pl/settings.ts
index 4b8e818e..161e7227 100644
--- a/src/locales/pl/settings.ts
+++ b/src/locales/pl/settings.ts
@@ -255,6 +255,9 @@ export const settings = {
showTrayIconDesc: 'Wyświetl ikonę Psysonic w obszarze powiadomień systemu/pasku menu.',
minimizeToTray: 'Minimalizuj do zasobnika systemowego',
minimizeToTrayDesc: 'Podczas zamykania okna, zamiast wyłączać Psysonic, pozostaw go uruchomionym w zasobniku systemowym.',
+ startMinimizedToTray: 'Uruchamiaj zminimalizowany w zasobniku',
+ startMinimizedToTrayDesc: 'Uruchamiaj Psysonic z ukrytym oknem głównym; pokaż je przez ikonę w zasobniku.',
+ startMinimizedToTrayRequiresTray: 'Najpierw włącz „Pokaż ikonę zasobnika” — bez niej nie otworzysz okna po ukrytym starcie.',
clockFormat: 'Format zegara',
clockFormatDesc: 'Wyświetlacz zegara ściennego używany przez kolejkę ETA i podgląd timera uśpienia.',
clockFormatAuto: 'Auto (ustawienia regionalne systemu)',
diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts
index e5ae60fd..6f011038 100644
--- a/src/locales/ro/settings.ts
+++ b/src/locales/ro/settings.ts
@@ -254,6 +254,9 @@ export const settings = {
showTrayIconDesc: 'Afișează iconița Psysonic în zona notificărilor de sistem / bara de meniu.',
minimizeToTray: 'Minimizează în Tavă',
minimizeToTrayDesc: 'La închiderea ferestrei, continuă rularea Psysonic în tava de sistem în loc de ieșire',
+ startMinimizedToTray: 'Pornește minimizat în tavă',
+ startMinimizedToTrayDesc: 'Lansează Psysonic cu fereastra principală ascunsă; folosește iconița din tavă pentru a o afișa.',
+ startMinimizedToTrayRequiresTray: 'Activează mai întâi „Afișează iconița tăvii” — fără tavă nu poți deschide fereastra după un start ascuns.',
clockFormat: 'Format Oră',
clockFormatDesc: 'Format al ceasului folosit pentru ETA-ul cozii și previzualizarea cronometrului de somn.',
clockFormatAuto: 'Automat (limba sistemului)',
diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts
index 156c2197..e25d12a9 100644
--- a/src/locales/ru/settings.ts
+++ b/src/locales/ru/settings.ts
@@ -256,6 +256,9 @@ export const settings = {
showTrayIconDesc: 'Показывать Psysonic в области уведомлений / строке меню.',
minimizeToTray: 'Сворачивать в трей',
minimizeToTrayDesc: 'При закрытии окна не выходить из приложения, а оставаться в трее.',
+ startMinimizedToTray: 'Запускать свёрнутым в трей',
+ startMinimizedToTrayDesc: 'При запуске скрывать главное окно; открыть приложение можно через иконку в трее.',
+ startMinimizedToTrayRequiresTray: 'Сначала включите «Иконка в трее» — без неё окно не открыть после скрытого запуска.',
clockFormat: 'Формат времени',
clockFormatDesc: 'Формат времени для ETA очереди и предпросмотра таймера сна.',
clockFormatAuto: 'Авто (системный)',
diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts
index 72abb1d4..2c4e8159 100644
--- a/src/locales/zh/settings.ts
+++ b/src/locales/zh/settings.ts
@@ -251,6 +251,9 @@ export const settings = {
showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。',
minimizeToTray: '最小化到托盘',
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
+ startMinimizedToTray: '启动时最小化到托盘',
+ startMinimizedToTrayDesc: '启动时隐藏主窗口;通过托盘图标显示窗口。',
+ startMinimizedToTrayRequiresTray: '请先启用“显示托盘图标”——隐藏启动后需要通过托盘打开窗口。',
clockFormat: '时间格式',
clockFormatDesc: '队列预计结束时间和睡眠定时器预览使用的时间格式。',
clockFormatAuto: '自动(跟随系统)',
diff --git a/src/store/authStore.settings.test.ts b/src/store/authStore.settings.test.ts
index fd2a8144..f1e1337f 100644
--- a/src/store/authStore.settings.test.ts
+++ b/src/store/authStore.settings.test.ts
@@ -54,6 +54,7 @@ describe('trivial pass-through setters', () => {
['setArtistBrowseCreditMode', 'artistBrowseCreditMode', 'track'],
['setShowTrayIcon', 'showTrayIcon', false],
['setMinimizeToTray', 'minimizeToTray', true],
+ ['setStartMinimizedToTray', 'startMinimizedToTray', true],
['setClockFormat', 'clockFormat', '24h'],
['setShowOrbitTrigger', 'showOrbitTrigger', false],
['setDiscordRichPresence', 'discordRichPresence', true],
@@ -313,6 +314,24 @@ describe('lyrics source setters', () => {
});
});
+describe('tray startup settings coupling', () => {
+ it('setStartMinimizedToTray enables showTrayIcon when it was off', () => {
+ useAuthStore.getState().setShowTrayIcon(false);
+ useAuthStore.getState().setStartMinimizedToTray(true);
+ const s = useAuthStore.getState();
+ expect(s.startMinimizedToTray).toBe(true);
+ expect(s.showTrayIcon).toBe(true);
+ });
+
+ it('setShowTrayIcon(false) clears startMinimizedToTray', () => {
+ useAuthStore.getState().setStartMinimizedToTray(true);
+ useAuthStore.getState().setShowTrayIcon(false);
+ const s = useAuthStore.getState();
+ expect(s.showTrayIcon).toBe(false);
+ expect(s.startMinimizedToTray).toBe(false);
+ });
+});
+
describe('mix filter setters — clamp to allowed range', () => {
it('setMixMinRatingSong / Album / Artist clamp out-of-range stars', () => {
useAuthStore.getState().setMixMinRatingSong(10);
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
index 4fbc66b8..d847bc81 100644
--- a/src/store/authStore.ts
+++ b/src/store/authStore.ts
@@ -72,6 +72,7 @@ export const useAuthStore = create()(
libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
showTrayIcon: true,
minimizeToTray: false,
+ startMinimizedToTray: false,
clockFormat: 'auto',
showOrbitTrigger: true,
discordRichPresence: false,
diff --git a/src/store/authStoreRehydrate.test.ts b/src/store/authStoreRehydrate.test.ts
index 10769b95..6f17f511 100644
--- a/src/store/authStoreRehydrate.test.ts
+++ b/src/store/authStoreRehydrate.test.ts
@@ -75,4 +75,14 @@ describe('computeAuthStoreRehydration — lyrics', () => {
{ id: 'netease', enabled: false },
]);
});
+
+ it('clears startMinimizedToTray when tray icon is off', () => {
+ const base = useAuthStore.getState();
+ const patch = computeAuthStoreRehydration({
+ ...base,
+ startMinimizedToTray: true,
+ showTrayIcon: false,
+ });
+ expect(patch.startMinimizedToTray).toBe(false);
+ });
});
diff --git a/src/store/authStoreRehydrate.ts b/src/store/authStoreRehydrate.ts
index ce674e4b..fc22a0ba 100644
--- a/src/store/authStoreRehydrate.ts
+++ b/src/store/authStoreRehydrate.ts
@@ -252,6 +252,9 @@ export function computeAuthStoreRehydration(state: AuthState): Partial void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
+ setStartMinimizedToTray: (v: boolean) => void;
setClockFormat: (v: ClockFormat) => void;
setShowOrbitTrigger: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
diff --git a/src/store/authUiAppearanceActions.ts b/src/store/authUiAppearanceActions.ts
index 5f241b67..c03081e7 100644
--- a/src/store/authUiAppearanceActions.ts
+++ b/src/store/authUiAppearanceActions.ts
@@ -17,6 +17,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
| 'setLibraryGridMaxColumns'
| 'setShowTrayIcon'
| 'setMinimizeToTray'
+ | 'setStartMinimizedToTray'
| 'setClockFormat'
| 'setShowOrbitTrigger'
| 'setUseCustomTitlebar'
@@ -45,8 +46,15 @@ export function createUiAppearanceActions(set: SetState): Pick<
setShowArtistImages: (v) => set({ showArtistImages: v }),
setArtistBrowseCreditMode: (v) => set({ artistBrowseCreditMode: v === 'track' ? 'track' : 'album' }),
setLibraryGridMaxColumns: (v) => set({ libraryGridMaxColumns: clampLibraryGridMaxColumns(v) }),
- setShowTrayIcon: (v) => set({ showTrayIcon: v }),
+ setShowTrayIcon: (v) => set({
+ showTrayIcon: v,
+ ...(v ? {} : { startMinimizedToTray: false }),
+ }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
+ setStartMinimizedToTray: (v) => set((state) => ({
+ startMinimizedToTray: v,
+ ...(v && !state.showTrayIcon ? { showTrayIcon: true } : {}),
+ })),
setClockFormat: (v) => set({ clockFormat: v }),
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index ef481c5d..95ed05a0 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -4,6 +4,7 @@ declare global {
interface Window {
__psyHidden?: boolean;
__psyBlurred?: boolean;
+ __psyStartMinimizedToTray?: boolean;
}
}