)}
{!searchQuery && <>
{/* ── Audio ────────────────────────────────────────────────────────────── */}
{activeTab === 'audio' && (
<>
{/* Audio Output Device */}
>
)}
{/* ── Lyrics ───────────────────────────────────────────────────────────── */}
{activeTab === 'lyrics' && (
<>
>
)}
{/* ── Integrations ─────────────────────────────────────────────────────── */}
{activeTab === 'integrations' && (
<>
{t('settings.integrationsPrivacyTitle')}
aus dem i18n-String — der Inhalt ist statisch
// und kommt nur aus unseren Locales, kein User-Input.
dangerouslySetInnerHTML={{ __html: t('settings.integrationsPrivacyBody') }}
/>
{/* Last.fm */}
}
>
{auth.lastfmSessionKey ? (
@{auth.lastfmUsername}
{lfmUserInfo && (
{t('settings.lfmScrobbles', { n: lfmUserInfo.playcount.toLocaleString() })}
{t('settings.lfmMemberSince', { year: new Date(lfmUserInfo.registeredAt * 1000).getFullYear() })}
)}
auth.disconnectLastfm()}
>
{t('settings.lfmDisconnect')}
) : lfmState === 'waiting' ? (
{t('settings.lfmConnecting')}
{ setLfmState('idle'); setLfmPendingToken(null); }}>
{t('common.cancel')}
) : (
{t('settings.lfmConnectDesc')}
{lfmState === 'error' && (
{lfmError}
)}
{t('settings.lfmConnect')}
)}
{/* Discord Rich Presence */}
}
>
{auth.discordRichPresence && (
<>
>
)}
{/* Bandsintown */}
}
>
{/* Now-Playing Share (Navidrome) */}
}
>
>
)}
{/* ── Personalisation ──────────────────────────────────────────────────── */}
{activeTab === 'personalisation' && (
<>
}
action={
useSidebarStore.getState().reset()}
data-tooltip={t('settings.sidebarReset')}
aria-label={t('settings.sidebarReset')}
>
}
>
}
action={
useArtistLayoutStore.getState().reset()}
data-tooltip={t('settings.artistLayoutReset')}
aria-label={t('settings.artistLayoutReset')}
>
}
>
}
action={
useHomeStore.getState().reset()}
data-tooltip={t('settings.sidebarReset')}
aria-label={t('settings.sidebarReset')}
>
}
>
>
)}
{/* ── Library (legacy 'general' + 'server') ────────────────────────────── */}
{activeTab === 'library' && (
<>
{/* Random Mix Blacklist */}
}
>
{t('settings.randomMixBlacklistDesc')}
{t('settings.randomMixBlacklistTitle')}
{auth.customGenreBlacklist.length === 0 ? (
{t('settings.randomMixBlacklistEmpty')}
) : (
auth.customGenreBlacklist.map(genre => (
{genre}
auth.setCustomGenreBlacklist(auth.customGenreBlacklist.filter(g => g !== genre))}
aria-label={`Remove ${genre}`}
>×
))
)}
setNewGenre(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && newGenre.trim()) {
const trimmed = newGenre.trim();
if (!auth.customGenreBlacklist.includes(trimmed)) {
auth.setCustomGenreBlacklist([...auth.customGenreBlacklist, trimmed]);
}
setNewGenre('');
}
}}
placeholder={t('settings.randomMixBlacklistPlaceholder')}
style={{ fontSize: 13 }}
/>
{
const trimmed = newGenre.trim();
if (trimmed && !auth.customGenreBlacklist.includes(trimmed)) {
auth.setCustomGenreBlacklist([...auth.customGenreBlacklist, trimmed]);
}
setNewGenre('');
}}
disabled={!newGenre.trim()}
>
{t('settings.randomMixBlacklistAdd')}
{t('settings.randomMixHardcodedTitle')}
{AUDIOBOOK_GENRES_DISPLAY.map(genre => (
{genre}
))}
{/* Ratings */}
}
>
{auth.mixMinRatingFilterEnabled && (
<>
{([
{ key: 'song', label: t('settings.ratingsMixMinSong'), value: auth.mixMinRatingSong, set: auth.setMixMinRatingSong },
{ key: 'album', label: t('settings.ratingsMixMinAlbum'), value: auth.mixMinRatingAlbum, set: auth.setMixMinRatingAlbum },
{ key: 'artist', label: t('settings.ratingsMixMinArtist'), value: auth.mixMinRatingArtist, set: auth.setMixMinRatingArtist },
] as const).map(row => (
{row.label}
))}
>
)}
>
)}
{/* ── Offline & Cache ──────────────────────────────────────────────────── */}
{activeTab === 'storage' && (
<>
{/* Offline Library (In-App) — includes cache settings */}
}
>
{t('settings.offlineDirDesc')}
{auth.offlineDownloadDir && (
auth.setOfflineDownloadDir('')}
data-tooltip={t('settings.offlineDirClear')}
style={{ color: 'var(--text-muted)', flexShrink: 0 }}
>
)}
{t('settings.offlineDirChange')}
{auth.offlineDownloadDir && (
{t('settings.offlineDirHint')}
)}
{(imageCacheBytes !== null || offlineCacheBytes !== null) && (
{t('settings.cacheUsedImages')}
{imageCacheBytes !== null ? formatBytes(imageCacheBytes) : '…'}
{t('settings.cacheUsedOffline')}
{offlineCacheBytes !== null ? formatBytes(offlineCacheBytes) : '…'}
)}
{t('settings.cacheMaxLabel')}
{
const v = Number(e.target.value);
if (v >= 100) auth.setMaxCacheMb(v);
}}
style={{ width: 80, padding: '4px 8px', fontSize: 13 }}
id="cache-size-input"
/>
MB
{showClearConfirm ? (
{t('settings.cacheClearWarning')}
{t('settings.cacheClearConfirm')}
setShowClearConfirm(false)} disabled={clearing}>
{t('settings.cacheClearCancel')}
) : (
setShowClearConfirm(true)}>
{t('settings.cacheClearBtn')}
)}
{t('settings.waveformCacheClearBtn')}
{/* Buffering */}
}
>
{t('settings.preloadHotCacheMutualExclusive')}
{/* Preload mode */}
{auth.preloadMode !== 'off' && (
<>
{(['balanced', 'early', 'custom'] as const).map(mode => (
auth.setPreloadMode(mode)}
>
{t(`settings.preload${mode.charAt(0).toUpperCase() + mode.slice(1)}` as any)}
))}
{auth.preloadMode === 'custom' && (
auth.setPreloadCustomSeconds(parseInt(e.target.value))}
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
/>
{t('settings.preloadCustomSeconds', { n: auth.preloadCustomSeconds })}
)}
>
)}
{/* Hot Cache */}
{auth.hotCacheEnabled && (
{auth.hotCacheDownloadDir && (
{
auth.setHotCacheDownloadDir('');
useHotCacheStore.setState({ entries: {} });
invoke('get_hot_cache_size', { customDir: null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0));
}}
data-tooltip={t('settings.hotCacheDirClear')}
style={{ color: 'var(--text-muted)', flexShrink: 0 }}
>
)}
{t('settings.hotCacheDirChange')}
{auth.hotCacheDownloadDir && (
{t('settings.hotCacheDirHint')}
)}
{t('settings.cacheUsedHot')}
{hotCacheBytes !== null ? formatBytes(hotCacheBytes) : '…'}
{t('settings.hotCacheTrackCount')}
{hotCacheTrackCount}
{
await clearHotCacheDisk(auth.hotCacheDownloadDir || null);
const b = await invoke('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).catch(() => 0);
setHotCacheBytes(b);
}}
>
{t('settings.hotCacheClearBtn')}
)}
{/* ZIP Export & Archiving */}
}
>
>
)}
{/* ── Appearance ───────────────────────────────────────────────────────── */}
{activeTab === 'appearance' && (
<>
}
>
{theme.enableThemeScheduler && (
{t('settings.themeSchedulerActiveHint')}
)}
theme.setTheme(v as any)} />
}
>
{theme.enableThemeScheduler && (() => {
const themeOptions = THEME_GROUPS.flatMap(g =>
g.themes.map(th => ({
value: th.id,
label: th.family ? `${th.family} ${th.label}` : th.label,
group: g.group,
}))
);
const use12h = i18n.language === 'en';
const hourOptions = Array.from({ length: 24 }, (_, i) => {
const value = String(i).padStart(2, '0');
const label = use12h
? `${i % 12 === 0 ? 12 : i % 12} ${i < 12 ? 'AM' : 'PM'}`
: value;
return { value, label };
});
const minuteOptions = ['00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'].map(m => ({ value: m, label: m }));
const dayH = theme.timeDayStart.split(':')[0];
const dayM = theme.timeDayStart.split(':')[1];
const nightH = theme.timeNightStart.split(':')[0];
const nightM = theme.timeNightStart.split(':')[1];
return (
{t('settings.themeSchedulerDayTheme')}
{t('settings.themeSchedulerDayStart')}
theme.setTimeDayStart(`${v}:${dayM}`)} options={hourOptions} />
:
theme.setTimeDayStart(`${dayH}:${v}`)} options={minuteOptions} />
{t('settings.themeSchedulerNightTheme')}
{t('settings.themeSchedulerNightStart')}
theme.setTimeNightStart(`${v}:${nightM}`)} options={hourOptions} />
:
theme.setTimeNightStart(`${nightH}:${v}`)} options={minuteOptions} />
);
})()}
}
>
{!IS_WINDOWS && (
<>
>
)}
{IS_LINUX && !isTilingWm && (
<>
>
)}
}
>
{t('settings.uiScaleLabel')}
{Math.round(fontStore.uiScale * 100)}%
{(() => {
const presets = [80, 90, 100, 110, 125, 150];
const currentPct = Math.round(fontStore.uiScale * 100);
let idx = presets.indexOf(currentPct);
if (idx < 0) {
// Snap legacy off-preset values to the closest preset.
idx = presets.reduce((best, p, i) =>
Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0);
}
return (
<>
fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)}
className="ui-scale-slider"
/>
{presets.map(p => {
const active = currentPct === p;
return (
fontStore.setUiScale(p / 100)}
>
{p}%
);
})}
>
);
})()}
}
>
{(
[
// Accessibility-first: OpenDyslexic at the top so dyslexic
// readers don't have to scroll past 14 sans-serifs to find it.
{ id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') },
{ id: 'inter', label: 'Inter', stack: "'Inter Variable', sans-serif" },
{ id: 'outfit', label: 'Outfit', stack: "'Outfit Variable', sans-serif" },
{ id: 'dm-sans', label: 'DM Sans', stack: "'DM Sans Variable', sans-serif" },
{ id: 'nunito', label: 'Nunito', stack: "'Nunito Variable', sans-serif" },
{ id: 'rubik', label: 'Rubik', stack: "'Rubik Variable', sans-serif" },
{ id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk Variable', sans-serif" },
{ id: 'figtree', label: 'Figtree', stack: "'Figtree Variable', sans-serif" },
{ id: 'manrope', label: 'Manrope', stack: "'Manrope Variable', sans-serif" },
{ id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans', stack: "'Plus Jakarta Sans Variable', sans-serif" },
{ id: 'lexend', label: 'Lexend', stack: "'Lexend Variable', sans-serif" },
{ id: 'geist', label: 'Geist', stack: "'Geist Variable', sans-serif" },
{ id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" },
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
{ id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" },
] as { id: FontId; label: string; stack: string; hint?: string }[]
).map(f => (
fontStore.setFont(f.id)}
>
{f.label}
{f.hint && (
{f.hint}
)}
))}
}
>
{auth.showFsArtistPortrait && (
)}
}
>
{t('settings.seekbarStyleDesc')}
{(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => (
auth.setSeekbarStyle(style)}
/>
))}
>
)}
{/* ── Input ────────────────────────────────────────────────────────────── */}
{activeTab === 'input' && (
<>
}
action={
{ kb.resetToDefaults(); setListeningFor(null); }}
data-tooltip={t('settings.shortcutsReset')}
aria-label={t('settings.shortcutsReset')}
>
}
>
{IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
const label = getLabel(t);
const bound = kb.bindings[action];
const isListening = listeningFor === action;
return (
{label}
{
if (isListening) { setListeningFor(null); return; }
setListeningFor(action);
const handler = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.code === 'Escape') {
setListeningFor(null);
window.removeEventListener('keydown', handler, true);
return;
}
const chord = buildInAppBinding(e);
if (!chord) return;
const existing = (Object.entries(kb.bindings) as [KeyAction, string | null][])
.find(([, c]) => c === chord)?.[0];
if (existing && existing !== action) kb.setBinding(existing, null);
kb.setBinding(action, chord);
setListeningFor(null);
window.removeEventListener('keydown', handler, true);
};
window.addEventListener('keydown', handler, true);
}}
className="keybind-badge"
style={{
minWidth: 72, padding: '3px 10px', borderRadius: 'var(--radius-sm)',
fontSize: 12, fontWeight: 600, fontFamily: 'monospace',
background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)',
color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)',
border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`,
cursor: 'pointer',
}}
>
{isListening ? t('settings.shortcutListening') : bound ? formatBinding(bound) : t('settings.shortcutUnbound')}
{bound && !isListening && (
kb.setBinding(action, null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }}
data-tooltip={t('settings.shortcutClear')}
>
)}
);
})}
}
description={t('settings.globalShortcutsNote')}
action={
{ gs.resetAll(); setListeningForGlobal(null); }}
data-tooltip={t('settings.shortcutsReset')}
aria-label={t('settings.shortcutsReset')}
>
}
>
{GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
const label = getLabel(t);
const bound = gs.shortcuts[action] ?? null;
const isListening = listeningForGlobal === action;
return (
{label}
{
if (isListening) { setListeningForGlobal(null); return; }
setListeningForGlobal(action);
const handler = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.code === 'Escape') {
setListeningForGlobal(null);
window.removeEventListener('keydown', handler, true);
return;
}
const shortcut = buildGlobalShortcut(e);
if (shortcut) {
gs.setShortcut(action, shortcut);
setListeningForGlobal(null);
window.removeEventListener('keydown', handler, true);
}
};
window.addEventListener('keydown', handler, true);
}}
className="keybind-badge"
style={{
minWidth: 120, padding: '3px 10px', borderRadius: 'var(--radius-sm)',
fontSize: 12, fontWeight: 600, fontFamily: 'monospace',
background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)',
color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)',
border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`,
cursor: 'pointer',
}}
>
{isListening ? t('settings.shortcutListening') : bound ? formatGlobalShortcut(bound) : t('settings.shortcutUnbound')}
{bound && !isListening && (
gs.setShortcut(action, null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }}
data-tooltip={t('settings.shortcutClear')}
>
)}
);
})}
>
)}
{/* ── Server ───────────────────────────────────────────────────────────── */}
{activeTab === 'servers' && (
<>
{t('settings.servers')}
{t('settings.serverCompatible')}
{auth.servers.length === 0 && !showAddForm ? (
{t('settings.noServers')}
) : (
{auth.servers.map((srv, srvIdx) => {
const isActive = srv.id === auth.activeServerId;
const status = connStatus[srv.id];
const isBefore = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && serverDropTarget.before;
const isAfter = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && !serverDropTarget.before;
return (
{serverListDisplayLabel(srv, auth.servers)}
{isActive && (
{t('settings.serverActive')}
)}
{srv.url.startsWith('https://') && (
)}
{srv.url.replace(/^https?:\/\//, '')}
{srv.username}
{status === 'ok' &&
}
{status === 'error' &&
}
{status === 'testing' &&
}
testConnection(srv)}
disabled={status === 'testing'}
>
{t('settings.testBtn')}
{!isActive && (
switchToServer(srv)}
disabled={status === 'testing'}
id={`settings-use-server-${srv.id}`}
>
{t('settings.useServer')}
)}
deleteServer(srv)}
data-tooltip={t('settings.deleteServer')}
id={`settings-delete-server-${srv.id}`}
>
{showAudiomuseNavidromeServerSetting(
auth.subsonicServerIdentityByServer[srv.id],
auth.instantMixProbeByServer[srv.id],
) && (
{t('settings.audiomuseTitle')}
{!!auth.audiomuseNavidromeByServer[srv.id] && auth.audiomuseNavidromeIssueByServer[srv.id] && (
)}
{
e.preventDefault();
void openUrl(AUDIOMUSE_NV_PLUGIN_URL);
}}
style={{ color: 'var(--accent)', textDecoration: 'underline' }}
/>
),
}}
/>
auth.setAudiomuseNavidromeEnabled(srv.id, e.target.checked)}
/>
)}
);
})}
)}
{showAddForm ? (
) : (
{
setPastedServerInvite(null);
setShowAddForm(true);
}}
id="settings-add-server-btn"
>
{t('settings.addServer')}
)}
>
)}
{/* ── System ───────────────────────────────────────────────────────────── */}
{activeTab === 'users' && ndAdminAuth && (
)}
{activeTab === 'system' && (
<>
}
>
i18n.changeLanguage(v)}
options={[
{ value: 'en', label: t('settings.languageEn') },
{ value: 'de', label: t('settings.languageDe') },
{ value: 'es', label: t('settings.languageEs') },
{ value: 'fr', label: t('settings.languageFr') },
{ value: 'nl', label: t('settings.languageNl') },
{ value: 'nb', label: t('settings.languageNb') },
{ value: 'ru', label: t('settings.languageRu') },
{ value: 'zh', label: t('settings.languageZh') },
]}
/>
{/* App-Verhalten (aus altem library/general Behavior-Block) */}
}
>
}
>
}
>
{t('settings.loggingModeDesc')}
auth.setLoggingMode(v as LoggingMode)}
options={[
{ value: 'off', label: t('settings.loggingModeOff') },
{ value: 'normal', label: t('settings.loggingModeNormal') },
{ value: 'debug', label: t('settings.loggingModeDebug') },
]}
/>
{auth.loggingMode === 'debug' && (
{t('settings.loggingExport')}
)}
}
>
{t('settings.aboutDesc')}
{t('settings.aboutLicense')}
{t('settings.aboutLicenseText')}
Stack
{t('settings.aboutBuiltWith')}
{t('settings.aboutMaintainersLabel')}
{MAINTAINERS.map(m => (
openUrl(`https://github.com/${m.github}`)}
>
@{m.github}
))}
{t('settings.aboutReleaseNotesLabel')}
{
useAuthStore.getState().setLastSeenChangelogVersion('');
navigate('/whats-new');
}}
style={{ color: 'var(--accent)', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
>
{t('settings.aboutReleaseNotesLink')}
openUrl('https://github.com/Psychotoxical/psysonic')}
>
{t('settings.aboutRepo')}
}
>
{[...CONTRIBUTORS].sort((a, b) => b.contributions.length - a.contributions.length).map(c => (
{ e.stopPropagation(); openUrl(`https://github.com/${c.github}`); }}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
e.preventDefault();
openUrl(`https://github.com/${c.github}`);
}
}}
>
@{c.github}
v{c.since}
·
{t('settings.aboutContributorsCount', { count: c.contributions.length })}
{c.contributions.map(item => {item} )}
))}
}
>
>
)}
>}