feat(tray): show now-playing track in tray + i18n menu labels (#395)

* feat(tray): show now-playing track in tray icon tooltip

Mirrors the current track to the tray tooltip ("Artist – Title") on every
play/pause/track change, falling back to "Psysonic" when nothing is playing.
The cached value survives tray hide/show via a new TrayTooltip state, and
is truncated to 127 chars for Windows NOTIFYICONDATA.szTip.

Closes #383


* feat(tray): linux fallback — disabled menu item shows now-playing track

AppIndicator on Linux has no hover-tooltip API, so the tooltip command was
a silent no-op there. Adds a disabled menu entry at the top of the tray
right-click menu that mirrors the same text. Updated in lockstep with the
Win/macOS tooltip via set_tray_tooltip.

* i18n(tray): localize tray menu labels

Adds a `tray` namespace in all 8 locales (en/de/fr/nl/zh/nb/ru/es) for
Play/Pause, Next/Previous, Show/Hide, Exit, and the Linux-only
"Nothing playing" placeholder.

Rust side: TrayMenuLabels state holds the cached translations and
TrayMenuItems holds the live MenuItem handles. New set_tray_menu_labels
command pushes new strings + applies them via set_text without rebuilding
the tray icon.

Frontend pushes the labels on mount and on every i18n.on('languageChanged').
This commit is contained in:
Frank Stellmacher
2026-05-01 12:58:16 +02:00
committed by GitHub
parent 20a083a9a6
commit 2ea22635e5
10 changed files with 302 additions and 10 deletions
+21 -1
View File
@@ -152,7 +152,7 @@ function shouldSuppressQueueResizerMouseDown(clientX: number, clientY: number, q
}
function AppShell() {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const isMobile = useIsMobile();
const [isWindowFullscreen, setIsWindowFullscreen] = useState(false);
const [isTilingWm, setIsTilingWm] = useState(false);
@@ -311,15 +311,35 @@ function AppShell() {
const title = `${state} ${currentTrack.artist} - ${currentTrack.title} | Psysonic`;
document.title = title;
await appWindow.setTitle(title);
await invoke('set_tray_tooltip', {
tooltip: `${currentTrack.artist} ${currentTrack.title}`,
}).catch(() => {});
} else {
document.title = 'Psysonic';
await appWindow.setTitle('Psysonic');
await invoke('set_tray_tooltip', { tooltip: '' }).catch(() => {});
}
} catch (err) {}
};
fn();
}, [currentTrack, isPlaying]);
useEffect(() => {
const apply = () => {
invoke('set_tray_menu_labels', {
playPause: t('tray.playPause'),
next: t('tray.nextTrack'),
previous: t('tray.previousTrack'),
showHide: t('tray.showHide'),
quit: t('tray.exitPsysonic'),
nothingPlaying: t('tray.nothingPlaying'),
}).catch(() => {});
};
apply();
i18n.on('languageChanged', apply);
return () => { i18n.off('languageChanged', apply); };
}, [t, i18n]);
// Post-update changelog is now surfaced via a dismissible banner in the
// sidebar (WhatsNewBanner) that links to the /whats-new page — no auto
// modal takeover on startup.