mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
feat(settings): start minimized to tray on cold launch (#1271)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
})();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
function reveal(attempt) {
|
||||
if (window.__psyStartMinimizedToTray) return;
|
||||
if (tryShowMainWindow()) return;
|
||||
if (attempt >= MAX_ATTEMPTS) return;
|
||||
window.setTimeout(function () {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -161,6 +161,18 @@ export function SystemTab() {
|
||||
checked={auth.minimizeToTray}
|
||||
onChange={auth.setMinimizeToTray}
|
||||
/>
|
||||
<div className="settings-section-divider" />
|
||||
<SettingsToggle
|
||||
label={t('settings.startMinimizedToTray')}
|
||||
desc={
|
||||
auth.showTrayIcon
|
||||
? t('settings.startMinimizedToTrayDesc')
|
||||
: t('settings.startMinimizedToTrayRequiresTray')
|
||||
}
|
||||
checked={auth.startMinimizedToTray}
|
||||
disabled={!auth.showTrayIcon}
|
||||
onChange={auth.setStartMinimizedToTray}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{IS_LINUX && (
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,9 @@ export const settings = {
|
||||
showTrayIconDesc: 'Показва иконата на Psysonic в областта за системни известия / менюто.',
|
||||
minimizeToTray: 'Минимизирай в системната лента',
|
||||
minimizeToTrayDesc: 'При затваряне на прозореца, запази Psysonic да работи в системната лента, вместо да излиза.',
|
||||
startMinimizedToTray: 'Стартирай минимизиран в трея',
|
||||
startMinimizedToTrayDesc: 'Стартирай Psysonic със скрит главен прозорец; покажи го чрез иконата в трея.',
|
||||
startMinimizedToTrayRequiresTray: 'Първо включи „Показвай икона в системната лента“ — без трей не можеш да отвориш прозореца след скрит старт.',
|
||||
clockFormat: 'Формат на часовника',
|
||||
clockFormatDesc: 'Показване на часа, използвано от прогнозното време на опашката и предварителния преглед на таймера за заспиване.',
|
||||
clockFormatAuto: 'Автоматично (системен регион)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -255,6 +255,9 @@ export const settings = {
|
||||
showTrayIconDesc: 'システム通知領域 / メニューバーに Psysonic アイコンを表示します。',
|
||||
minimizeToTray: 'トレイへ最小化',
|
||||
minimizeToTrayDesc: 'ウィンドウを閉じたとき、終了せずにシステムトレイで Psysonic を実行し続けます。',
|
||||
startMinimizedToTray: 'トレイで起動',
|
||||
startMinimizedToTrayDesc: 'メインウィンドウを非表示のまま起動します。表示するにはトレイアイコンを使います。',
|
||||
startMinimizedToTrayRequiresTray: '先に「トレイアイコンを表示」を有効にしてください。非表示起動後はトレイから開けます。',
|
||||
clockFormat: '時計形式',
|
||||
clockFormatDesc: 'キューの終了予定時刻とスリープタイマーのプレビューに使う時計表示です。',
|
||||
clockFormatAuto: '自動 (システムロケール)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -256,6 +256,9 @@ export const settings = {
|
||||
showTrayIconDesc: 'Показывать Psysonic в области уведомлений / строке меню.',
|
||||
minimizeToTray: 'Сворачивать в трей',
|
||||
minimizeToTrayDesc: 'При закрытии окна не выходить из приложения, а оставаться в трее.',
|
||||
startMinimizedToTray: 'Запускать свёрнутым в трей',
|
||||
startMinimizedToTrayDesc: 'При запуске скрывать главное окно; открыть приложение можно через иконку в трее.',
|
||||
startMinimizedToTrayRequiresTray: 'Сначала включите «Иконка в трее» — без неё окно не открыть после скрытого запуска.',
|
||||
clockFormat: 'Формат времени',
|
||||
clockFormatDesc: 'Формат времени для ETA очереди и предпросмотра таймера сна.',
|
||||
clockFormatAuto: 'Авто (системный)',
|
||||
|
||||
@@ -251,6 +251,9 @@ export const settings = {
|
||||
showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。',
|
||||
minimizeToTray: '最小化到托盘',
|
||||
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
|
||||
startMinimizedToTray: '启动时最小化到托盘',
|
||||
startMinimizedToTrayDesc: '启动时隐藏主窗口;通过托盘图标显示窗口。',
|
||||
startMinimizedToTrayRequiresTray: '请先启用“显示托盘图标”——隐藏启动后需要通过托盘打开窗口。',
|
||||
clockFormat: '时间格式',
|
||||
clockFormatDesc: '队列预计结束时间和睡眠定时器预览使用的时间格式。',
|
||||
clockFormatAuto: '自动(跟随系统)',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -72,6 +72,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
|
||||
showTrayIcon: true,
|
||||
minimizeToTray: false,
|
||||
startMinimizedToTray: false,
|
||||
clockFormat: 'auto',
|
||||
showOrbitTrigger: true,
|
||||
discordRichPresence: false,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,6 +252,9 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
return {
|
||||
...mediaDirMigrated,
|
||||
...musicNetworkMigrated,
|
||||
...(state.startMinimizedToTray && state.showTrayIcon === false
|
||||
? { startMinimizedToTray: false as const }
|
||||
: {}),
|
||||
mixMinRatingSong: clampMixFilterMinStars(state.mixMinRatingSong as number),
|
||||
mixMinRatingAlbum: clampMixFilterMinStars(state.mixMinRatingAlbum as number),
|
||||
mixMinRatingArtist: clampMixFilterMinStars(state.mixMinRatingArtist as number),
|
||||
|
||||
@@ -188,6 +188,8 @@ export interface AuthState {
|
||||
libraryGridMaxColumns: number;
|
||||
showTrayIcon: boolean;
|
||||
minimizeToTray: boolean;
|
||||
/** Cold start: keep the main window hidden and run in the system tray. */
|
||||
startMinimizedToTray: boolean;
|
||||
clockFormat: ClockFormat;
|
||||
/** Whether the "Orbit" topbar trigger is rendered. Users who never
|
||||
* touch Orbit can hide it so the header stays uncluttered. */
|
||||
@@ -416,6 +418,7 @@ export interface AuthState {
|
||||
setLibraryGridMaxColumns: (v: number) => 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;
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@ declare global {
|
||||
interface Window {
|
||||
__psyHidden?: boolean;
|
||||
__psyBlurred?: boolean;
|
||||
__psyStartMinimizedToTray?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user