Settings — own Audio categories + Queue Settings consolidation (#1130)

* refactor(settings): promote Normalization and Track transitions to own Audio categories

Pull Normalization and Track transitions out of the combined Playback
section into their own top-level SettingsSubSection categories, placed
directly under Audio Output Device. Both follow the established reusable
pattern (SettingsSubSection header + title-less SettingsGroup inside a
single-group settings-card, so the frame-collapse CSS applies).

- New TrackTransitionsBlock extracted from PlaybackBehaviorBlock; the
  latter now holds only the Queue behaviour toggle (slated to move to
  Personalisation).
- NormalizationBlock's SettingsGroup is now title-less; the section
  header and description name it.
- Split the single audio search-index row into three (Normalization /
  Track transitions / Playback) so crossfade/replaygain/lufs keywords
  focus the right section.

* refactor(settings): consolidate Queue Settings under Personalisation, drop Audio Playback section

Combine Queue Display Mode and Queue Toolbar under one 'Queue Settings'
category in the Personalisation tab, and move the Queue behaviour toggle
(preservePlayNextOrder) there from Audio. The now-empty Audio Playback
section is removed.

- New 'Queue Settings' SettingsSubSection holds three titled groups:
  Queue Display Mode, Queue behaviour, and (advanced-only) Queue Toolbar.
  The toolbar group keeps its reset, now via a new optional 'action' slot
  on SettingsGroup (+ .settings-group-title-action CSS).
- Delete PlaybackBehaviorBlock; its single toggle is inlined.
- Search index: drop the audio 'playbackTitle' row; the personalisation
  queue row now points at 'queueSettingsTitle' with merged keywords.
- i18n: add settings.queueSettingsTitle to all 9 locales.

* fix(settings): align queue-toolbar separator label and restore Advanced badge

- QueueToolbarCustomizer: the separator row rendered a 1px rule where other
  rows have a 16px icon, so its label sat shifted left. Reserve the full
  16px icon column (1px rule centred) so the label lines up. (Preexisting.)
- SettingsGroup gains an optional 'advanced' flag rendering the Advanced
  badge; badge + action now share a right-aligned title-end slot
  (.settings-group-title-end), so the badge sits just left of the reset
  button. Restores the Advanced indicator the Queue Toolbar lost when it
  moved from a SettingsSubSection into a group.

* fix(audio): hide the output-device category on macOS instead of showing a notice

Playback is pinned to the system default on macOS, so the picker showed a
notice explaining it does nothing there. Gate the whole Audio Output Device
category out on macOS (`!IS_MACOS` in AudioTab) and drop the now-dead notice
branch + the `audioOutputDeviceMacNotice` string from all 9 locales. The
device-probe hook already short-circuits on macOS, so no work is wasted.

* refactor(settings): box the sidebar customizer groups consistently

The sidebar display toggles sat as a bare div and looked unfinished. Box
them in a SettingsGroup, with the nav-item drag list in a second group.

Render the groups directly in the sub-section content (no settings-card
wrapper) — matching the other Personalisation customizers, which use bare
SettingsGroups. Also drop the settings-card around Queue Settings for the
same reason, so every Personalisation section's boxes share one width and
inset instead of the card-wrapped ones sitting narrower/indented.

* docs(settings): changelog, what's new, and credits for the settings reorg (#1130)

Fold the Audio/Personalisation reorganization into the existing
'Settings — consistent grouped layout' changelog entry (now #1126 + #1130)
and the matching What's New highlight, and add a consolidated settings-
refactor line to the contributor credits.
This commit is contained in:
Psychotoxical
2026-06-19 13:57:08 +02:00
committed by GitHub
parent 4225146a16
commit c428d37e0e
24 changed files with 281 additions and 230 deletions
+4 -1
View File
@@ -47,11 +47,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Settings — consistent grouped layout ### Settings — consistent grouped layout
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1126](https://github.com/Psychotoxical/psysonic/pull/1126)** **By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1126](https://github.com/Psychotoxical/psysonic/pull/1126) and PR [#1130](https://github.com/Psychotoxical/psysonic/pull/1130)**
* The settings tabs now group related controls into clearly bordered, labelled panels for a more consistent, easier-to-scan layout — across Appearance, System, Audio, Storage, Library, Integrations, Music Network, Lyrics, Personalisation, Input and Themes. Standalone toggles are left as they were, and a few duplicated section titles are gone. * The settings tabs now group related controls into clearly bordered, labelled panels for a more consistent, easier-to-scan layout — across Appearance, System, Audio, Storage, Library, Integrations, Music Network, Lyrics, Personalisation, Input and Themes. Standalone toggles are left as they were, and a few duplicated section titles are gone.
* The **Lucky Mix menu** toggle moved from the Library tab to the sidebar customizer, alongside the other navigation toggles. * The **Lucky Mix menu** toggle moved from the Library tab to the sidebar customizer, alongside the other navigation toggles.
* The **Native Hi-Res Playback** description now explains what turning it on actually does — play each track at its original sample rate, matching the audio device to the file, instead of resampling everything to 44.1 kHz. The old wording described the off state and read as if the option forced 44.1 kHz. * The **Native Hi-Res Playback** description now explains what turning it on actually does — play each track at its original sample rate, matching the audio device to the file, instead of resampling everything to 44.1 kHz. The old wording described the off state and read as if the option forced 44.1 kHz.
* **Settings → Audio**: **Normalization** and **Track transitions** are now their own top-level categories (directly under Audio Output Device) instead of being grouped together inside one *Playback* section.
* **Settings → Personalisation** gains a **Queue Settings** category that brings the queue display mode, the queue toolbar customizer, and the **Preserve "Play Next" order** toggle (moved here from Audio) together in one place.
* On macOS, the **Audio Output Device** category is now hidden rather than showing a notice — playback there always follows the system output device.
## Fixed ## Fixed
+1
View File
@@ -28,6 +28,7 @@ Within each section, order by **user impact** (most noticeable first) — not PR
### Settings — tidier and easier to scan ### Settings — tidier and easier to scan
- Settings are grouped into clear, labelled panels so related options sit together — less hunting around. The **Native Hi-Res Playback** option now explains in plain language what it actually does. - Settings are grouped into clear, labelled panels so related options sit together — less hunting around. The **Native Hi-Res Playback** option now explains in plain language what it actually does.
- **Normalization** and **Track transitions** are now their own sections under **Settings → Audio**, and the queue options (display mode, toolbar, and Play-Next order) are gathered into one **Queue Settings** group under **Personalisation**.
## Fixed ## Fixed
+29 -14
View File
@@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Music2, Sliders, Waves } from 'lucide-react'; import { Blend, Gauge, Sliders, Volume2, Waves } from 'lucide-react';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import Equalizer from '../Equalizer'; import Equalizer from '../Equalizer';
import SettingsSubSection from '../SettingsSubSection'; import SettingsSubSection from '../SettingsSubSection';
@@ -8,11 +8,11 @@ import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle'; import { SettingsToggle } from './SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../../utils/audio/loudnessPreAnalysisSlider'; import { effectiveLoudnessPreAnalysisAttenuationDb } from '../../utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '../../hooks/useAudioDevicesProbe'; import { useAudioDevicesProbe } from '../../hooks/useAudioDevicesProbe';
import { IS_MACOS } from '../../utils/platform';
import { AudioOutputDeviceSection } from './audio/AudioOutputDeviceSection'; import { AudioOutputDeviceSection } from './audio/AudioOutputDeviceSection';
import { NormalizationBlock } from './audio/NormalizationBlock'; import { NormalizationBlock } from './audio/NormalizationBlock';
import { Gauge } from 'lucide-react';
import { PlaybackBehaviorBlock } from './audio/PlaybackBehaviorBlock';
import { PlaybackRateBlock } from './audio/PlaybackRateBlock'; import { PlaybackRateBlock } from './audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from './audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from './audio/TrackPreviewsSection'; import { TrackPreviewsSection } from './audio/TrackPreviewsSection';
export function AudioTab() { export function AudioTab() {
@@ -37,6 +37,9 @@ export function AudioTab() {
return ( return (
<> <>
{/* Output-device picker is hidden on macOS — the stream is pinned to the
system default there, so the whole category is gated out. */}
{!IS_MACOS && (
<AudioOutputDeviceSection <AudioOutputDeviceSection
audioDevices={audioDevices} audioDevices={audioDevices}
osDefaultAudioDeviceId={osDefaultAudioDeviceId} osDefaultAudioDeviceId={osDefaultAudioDeviceId}
@@ -46,6 +49,29 @@ export function AudioTab() {
refreshAudioDevices={refreshAudioDevices} refreshAudioDevices={refreshAudioDevices}
t={t} t={t}
/> />
)}
{/* Normalization — loudness levelling (own category) */}
<SettingsSubSection
title={t('settings.normalization')}
description={t('settings.normalizationDesc')}
icon={<Volume2 size={16} />}
>
<div className="settings-card">
<NormalizationBlock preAnalysisEffectiveDb={preAnalysisEffectiveDb} t={t} />
</div>
</SettingsSubSection>
{/* Track transitions — crossfade / gapless / AutoDJ (own category) */}
<SettingsSubSection
title={t('settings.transitionsTitle')}
description={t('settings.transitionsDesc')}
icon={<Blend size={16} />}
>
<div className="settings-card">
<TrackTransitionsBlock t={t} />
</div>
</SettingsSubSection>
{/* Native Hi-Res Playback */} {/* Native Hi-Res Playback */}
<SettingsSubSection <SettingsSubSection
@@ -89,17 +115,6 @@ export function AudioTab() {
</div> </div>
</SettingsSubSection> </SettingsSubSection>
{/* Replay Gain + Crossfade + Gapless */}
<SettingsSubSection
title={t('settings.playbackTitle')}
icon={<Music2 size={16} />}
>
<div className="settings-card">
<NormalizationBlock preAnalysisEffectiveDb={preAnalysisEffectiveDb} t={t} />
<PlaybackBehaviorBlock t={t} />
</div>
</SettingsSubSection>
<TrackPreviewsSection t={t} /> <TrackPreviewsSection t={t} />
</> </>
); );
+22 -11
View File
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Disc3, LayoutGrid, ListMusic, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react'; import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { useArtistLayoutStore } from '../../store/artistLayoutStore'; import { useArtistLayoutStore } from '../../store/artistLayoutStore';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { useHomeStore } from '../../store/homeStore'; import { useHomeStore } from '../../store/homeStore';
@@ -21,6 +21,9 @@ export function PersonalisationTab() {
const { t } = useTranslation(); const { t } = useTranslation();
const queueDisplayMode = useAuthStore(s => s.queueDisplayMode); const queueDisplayMode = useAuthStore(s => s.queueDisplayMode);
const setQueueDisplayMode = useAuthStore(s => s.setQueueDisplayMode); const setQueueDisplayMode = useAuthStore(s => s.setQueueDisplayMode);
const preservePlayNextOrder = useAuthStore(s => s.preservePlayNextOrder);
const setPreservePlayNextOrder = useAuthStore(s => s.setPreservePlayNextOrder);
const advancedSettingsEnabled = useAuthStore(s => s.advancedSettingsEnabled);
return ( return (
<> <>
<SettingsSubSection <SettingsSubSection
@@ -39,9 +42,7 @@ export function PersonalisationTab() {
</button> </button>
} }
> >
<SettingsGroup>
<SidebarCustomizer /> <SidebarCustomizer />
</SettingsGroup>
</SettingsSubSection> </SettingsSubSection>
<SettingsSubSection <SettingsSubSection
@@ -87,12 +88,14 @@ export function PersonalisationTab() {
</SettingsGroup> </SettingsGroup>
</SettingsSubSection> </SettingsSubSection>
{/* Queue Settings — display mode, queue behaviour and (advanced) the
toolbar customizer, grouped under one category. */}
<SettingsSubSection <SettingsSubSection
title={t('settings.queueModeTitle')} title={t('settings.queueSettingsTitle')}
icon={<ListOrdered size={16} />} icon={<ListOrdered size={16} />}
> >
<div className="settings-card"> <>
<SettingsGroup> <SettingsGroup title={t('settings.queueModeTitle')}>
{/* Three mutually exclusive modes — exactly one is always active, so {/* Three mutually exclusive modes — exactly one is always active, so
turning one on turns the others off; the active one cannot be turning one on turns the others off; the active one cannot be
switched off directly (ignore the uncheck). */} switched off directly (ignore the uncheck). */}
@@ -117,12 +120,19 @@ export function PersonalisationTab() {
onChange={c => { if (c) setQueueDisplayMode('timeline'); }} onChange={c => { if (c) setQueueDisplayMode('timeline'); }}
/> />
</SettingsGroup> </SettingsGroup>
</div>
</SettingsSubSection>
<SettingsSubSection <SettingsGroup title={t('settings.queueBehaviourTitle')}>
<SettingsToggle
label={t('settings.preservePlayNextOrder')}
desc={t('settings.preservePlayNextOrderDesc')}
checked={preservePlayNextOrder}
onChange={setPreservePlayNextOrder}
/>
</SettingsGroup>
{advancedSettingsEnabled && (
<SettingsGroup
title={t('settings.queueToolbarTitle')} title={t('settings.queueToolbarTitle')}
icon={<ListMusic size={16} />}
advanced advanced
action={ action={
<button <button
@@ -137,9 +147,10 @@ export function PersonalisationTab() {
</button> </button>
} }
> >
<SettingsGroup>
<QueueToolbarCustomizer /> <QueueToolbarCustomizer />
</SettingsGroup> </SettingsGroup>
)}
</>
</SettingsSubSection> </SettingsSubSection>
<SettingsSubSection <SettingsSubSection
@@ -124,7 +124,11 @@ export function QueueToolbarCustomizer() {
{Icon ? ( {Icon ? (
<Icon size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <Icon size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
) : ( ) : (
<div style={{ width: 1, height: 16, background: 'var(--border-subtle)', flexShrink: 0 }} /> // Reserve the same 16px icon column so the label lines up with the
// other rows; the 1px rule is centred within it.
<div style={{ width: 16, display: 'flex', justifyContent: 'center', flexShrink: 0 }}>
<div style={{ width: 1, height: 16, background: 'var(--border-subtle)' }} />
</div>
)} )}
<span style={{ flex: 1, fontSize: 14 }}>{label}</span> <span style={{ flex: 1, fontSize: 14 }}>{label}</span>
<label className="toggle-switch" aria-label={label}> <label className="toggle-switch" aria-label={label}>
+19 -1
View File
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
/** Accent uppercase header. Omit for a plain boxed panel (no header) — /** Accent uppercase header. Omit for a plain boxed panel (no header) —
@@ -9,6 +10,12 @@ interface Props {
icon?: React.ReactNode; icon?: React.ReactNode;
/** Optional one-line description shown under the title. */ /** Optional one-line description shown under the title. */
desc?: string; desc?: string;
/** Show an "Advanced" badge after the title (e.g. for a group that only
* renders in Advanced Mode). Ignored when `title` is omitted. */
advanced?: boolean;
/** Optional right-aligned node in the title row (e.g. a reset button).
* Ignored when `title` is omitted. */
action?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
} }
@@ -18,13 +25,24 @@ interface Props {
* settings card. Wraps the `.settings-group` styles so the look stays * settings card. Wraps the `.settings-group` styles so the look stays
* consistent everywhere it is used (Audio, Appearance, Library, …). * consistent everywhere it is used (Audio, Appearance, Library, …).
*/ */
export function SettingsGroup({ title, icon, desc, children }: Props) { export function SettingsGroup({ title, icon, desc, advanced, action, children }: Props) {
const { t } = useTranslation();
return ( return (
<div className="settings-group"> <div className="settings-group">
{title && ( {title && (
<div className="settings-group-title"> <div className="settings-group-title">
{icon && <span className="settings-group-title-icon">{icon}</span>} {icon && <span className="settings-group-title-icon">{icon}</span>}
{title} {title}
{(advanced || action) && (
<span className="settings-group-title-end">
{advanced && (
<span className="settings-sub-section-advanced-badge">
{t('settings.advancedBadge')}
</span>
)}
{action}
</span>
)}
</div> </div>
)} )}
<div className="settings-group-body"> <div className="settings-group-body">
@@ -7,6 +7,7 @@ import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '.
import { useLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable';
import { ALL_NAV_ITEMS } from '../../config/navItems'; import { ALL_NAV_ITEMS } from '../../config/navItems';
import { applySidebarDropReorder } from '../../utils/componentHelpers/sidebarNavReorder'; import { applySidebarDropReorder } from '../../utils/componentHelpers/sidebarNavReorder';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle'; import { SettingsToggle } from './SettingsToggle';
type DropTarget = { idx: number; before: boolean; section: 'library' | 'system' } | null; type DropTarget = { idx: number; before: boolean; section: 'library' | 'system' } | null;
@@ -128,7 +129,7 @@ export function SidebarCustomizer() {
return ( return (
<> <>
<div style={{ marginBottom: '1rem' }}> <SettingsGroup>
<SettingsToggle <SettingsToggle
label={t('settings.randomNavSplitTitle')} label={t('settings.randomNavSplitTitle')}
desc={t('settings.randomNavSplitDesc')} desc={t('settings.randomNavSplitDesc')}
@@ -148,7 +149,9 @@ export function SidebarCustomizer() {
checked={showLuckyMixMenu} checked={showLuckyMixMenu}
onChange={setShowLuckyMixMenu} onChange={setShowLuckyMixMenu}
/> />
</div> </SettingsGroup>
<SettingsGroup>
<div ref={containerRef} onMouseMove={handleMouseMove} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div ref={containerRef} onMouseMove={handleMouseMove} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* Library block */} {/* Library block */}
<div style={{ padding: '4px 0' }}> <div style={{ padding: '4px 0' }}>
@@ -164,6 +167,7 @@ export function SidebarCustomizer() {
</div> </div>
</div> </div>
</div> </div>
</SettingsGroup>
</> </>
); );
} }
@@ -6,7 +6,6 @@ import CustomSelect from '../../CustomSelect';
import SettingsSubSection from '../../SettingsSubSection'; import SettingsSubSection from '../../SettingsSubSection';
import { SettingsGroup } from '../SettingsGroup'; import { SettingsGroup } from '../SettingsGroup';
import { useAuthStore } from '../../../store/authStore'; import { useAuthStore } from '../../../store/authStore';
import { IS_MACOS } from '../../../utils/platform';
import { buildAudioDeviceSelectOptions } from '../../../utils/audio/audioDeviceLabels'; import { buildAudioDeviceSelectOptions } from '../../../utils/audio/audioDeviceLabels';
interface Props { interface Props {
@@ -20,8 +19,9 @@ interface Props {
} }
/** /**
* Audio output device picker. macOS is hard-pinned to the system default, * Audio output device picker. Not rendered on macOS — the audio stream is
* so the picker collapses to a notice on that platform. * pinned to the system default there, so the whole category is gated out by
* the caller (`AudioTab`).
* *
* The device switch is best-effort: if `audio_set_device` rejects (e.g. * The device switch is best-effort: if `audio_set_device` rejects (e.g.
* device disappeared) we leave the previous selection in the store. * device disappeared) we leave the previous selection in the store.
@@ -45,12 +45,6 @@ export function AudioOutputDeviceSection({
> >
<div className="settings-card"> <div className="settings-card">
<SettingsGroup> <SettingsGroup>
{IS_MACOS ? (
<div style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.55 }}>
{t('settings.audioOutputDeviceMacNotice')}
</div>
) : (
<>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}> <div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
{t('settings.audioOutputDeviceDesc')} {t('settings.audioOutputDeviceDesc')}
</div> </div>
@@ -86,8 +80,6 @@ export function AudioOutputDeviceSection({
<RotateCcw size={15} className={devicesLoading ? 'spin' : ''} /> <RotateCcw size={15} className={devicesLoading ? 'spin' : ''} />
</button> </button>
</div> </div>
</>
)}
</SettingsGroup> </SettingsGroup>
</div> </div>
</SettingsSubSection> </SettingsSubSection>
@@ -24,15 +24,16 @@ interface Props {
* *
* Switching engines clears the other engine's enabled flag so only one * Switching engines clears the other engine's enabled flag so only one
* can be live at a time. * can be live at a time.
*
* Rendered as its own top-level "Normalization" category in the Audio tab, so
* the boxed `SettingsGroup` is title-less — the `SettingsSubSection` header and
* description name it.
*/ */
export function NormalizationBlock({ preAnalysisEffectiveDb, t }: Props) { export function NormalizationBlock({ preAnalysisEffectiveDb, t }: Props) {
const auth = useAuthStore(); const auth = useAuthStore();
return ( return (
<SettingsGroup <SettingsGroup>
title={t('settings.normalization', { defaultValue: 'Normalization' })}
desc={t('settings.normalizationDesc')}
>
<div className="settings-segmented" style={{ marginBottom: auth.normalizationEngine === 'off' ? 0 : '0.85rem' }}> <div className="settings-segmented" style={{ marginBottom: auth.normalizationEngine === 'off' ? 0 : '0.85rem' }}>
<button <button
type="button" type="button"
@@ -1,98 +0,0 @@
import React from 'react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../../../store/authStore';
import {
getTransitionMode,
setTransitionMode,
type TransitionMode,
} from '../../../utils/playback/playbackTransition';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsToggle } from '../SettingsToggle';
interface Props {
t: TFunction;
}
/**
* Track-transition picker. Crossfade, AutoDJ and Gapless are mutually
* exclusive — only one can be active — so they are presented as a single
* `Off | Gapless | Crossfade | AutoDJ` segmented control (mirroring the
* Normalization picker above it) backed by the shared transition-mode helper.
*
* Classic crossfade exposes the seconds slider; AutoDJ is content-driven and
* has no duration to configure (just a short explainer). The
* `preservePlayNextOrder` toggle is independent and grouped under its own
* "Queue behaviour" heading at the bottom.
*/
export function PlaybackBehaviorBlock({ t }: Props) {
const auth = useAuthStore();
const mode = getTransitionMode(auth);
const transitions: { id: TransitionMode; label: string }[] = [
{ id: 'none', label: t('settings.transitionOff') },
{ id: 'gapless', label: t('settings.gapless') },
{ id: 'crossfade', label: t('settings.crossfade') },
{ id: 'autodj', label: t('settings.autoDj') },
];
return (
<>
<SettingsGroup title={t('settings.transitionsTitle')} desc={t('settings.transitionsDesc')}>
<div className="settings-segmented">
{transitions.map(item => (
<button
key={item.id}
type="button"
className={`btn ${mode === item.id ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setTransitionMode(item.id)}
>
{item.label}
</button>
))}
</div>
{mode === 'crossfade' && (
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<input
type="range"
min={0.1}
max={10}
step={0.1}
value={auth.crossfadeSecs}
onChange={e => auth.setCrossfadeSecs(parseFloat(e.target.value))}
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
id="crossfade-secs-slider"
/>
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 36 }}>
{t('settings.crossfadeSecs', { n: auth.crossfadeSecs.toFixed(1) })}
</span>
</div>
)}
{mode === 'autodj' && (
<>
<div style={{ paddingLeft: '1rem', fontSize: 12, color: 'var(--text-muted)', marginTop: '0.7rem' }}>
{t('settings.autoDjDesc')}
</div>
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem' }}>
<SettingsToggle
label={t('settings.autodjSmoothSkip')}
desc={t('settings.autodjSmoothSkipDesc')}
checked={auth.autodjSmoothSkip}
onChange={auth.setAutodjSmoothSkip}
/>
</div>
</>
)}
</SettingsGroup>
<SettingsGroup title={t('settings.queueBehaviourTitle')}>
<SettingsToggle
label={t('settings.preservePlayNextOrder')}
desc={t('settings.preservePlayNextOrderDesc')}
checked={auth.preservePlayNextOrder}
onChange={auth.setPreservePlayNextOrder}
/>
</SettingsGroup>
</>
);
}
@@ -0,0 +1,90 @@
import React from 'react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../../../store/authStore';
import {
getTransitionMode,
setTransitionMode,
type TransitionMode,
} from '../../../utils/playback/playbackTransition';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsToggle } from '../SettingsToggle';
interface Props {
t: TFunction;
}
/**
* Track-transition picker. Crossfade, AutoDJ and Gapless are mutually
* exclusive — only one can be active — so they are presented as a single
* `Off | Gapless | Crossfade | AutoDJ` segmented control backed by the shared
* transition-mode helper.
*
* Classic crossfade exposes the seconds slider; AutoDJ is content-driven and
* has no duration to configure (just a short explainer + the smooth-skip
* toggle).
*
* Rendered as its own top-level "Track transitions" category in the Audio tab,
* so the boxed `SettingsGroup` is title-less — the `SettingsSubSection` header
* names it.
*/
export function TrackTransitionsBlock({ t }: Props) {
const auth = useAuthStore();
const mode = getTransitionMode(auth);
const transitions: { id: TransitionMode; label: string }[] = [
{ id: 'none', label: t('settings.transitionOff') },
{ id: 'gapless', label: t('settings.gapless') },
{ id: 'crossfade', label: t('settings.crossfade') },
{ id: 'autodj', label: t('settings.autoDj') },
];
return (
<SettingsGroup>
<div className="settings-segmented">
{transitions.map(item => (
<button
key={item.id}
type="button"
className={`btn ${mode === item.id ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setTransitionMode(item.id)}
>
{item.label}
</button>
))}
</div>
{mode === 'crossfade' && (
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<input
type="range"
min={0.1}
max={10}
step={0.1}
value={auth.crossfadeSecs}
onChange={e => auth.setCrossfadeSecs(parseFloat(e.target.value))}
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
id="crossfade-secs-slider"
/>
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 36 }}>
{t('settings.crossfadeSecs', { n: auth.crossfadeSecs.toFixed(1) })}
</span>
</div>
)}
{mode === 'autodj' && (
<>
<div style={{ paddingLeft: '1rem', fontSize: 12, color: 'var(--text-muted)', marginTop: '0.7rem' }}>
{t('settings.autoDjDesc')}
</div>
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem' }}>
<SettingsToggle
label={t('settings.autodjSmoothSkip')}
desc={t('settings.autodjSmoothSkipDesc')}
checked={auth.autodjSmoothSkip}
onChange={auth.setAutodjSmoothSkip}
/>
</div>
</>
)}
</SettingsGroup>
);
}
+3 -2
View File
@@ -37,7 +37,8 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit' }, { tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit' },
{ tab: 'audio', titleKey: 'settings.eqTitle', keywords: 'equalizer eq bass treble autoeq filter pre-gain' }, { tab: 'audio', titleKey: 'settings.eqTitle', keywords: 'equalizer eq bass treble autoeq filter pre-gain' },
{ tab: 'audio', titleKey: 'settings.playbackRateTitle', keywords: 'speed playback rate tempo pitch varispeed preserve corrected time stretch' }, { tab: 'audio', titleKey: 'settings.playbackRateTitle', keywords: 'speed playback rate tempo pitch varispeed preserve corrected time stretch' },
{ tab: 'audio', titleKey: 'settings.playbackTitle', keywords: 'playback crossfade autodj auto dj smart crossfade gapless replaygain replay gain volume trim silence' }, { tab: 'audio', titleKey: 'settings.normalization', keywords: 'normalization normalisation loudness volume leveling level replaygain replay gain lufs pre-gain' },
{ tab: 'audio', titleKey: 'settings.transitionsTitle', keywords: 'track transitions crossfade autodj auto dj smart crossfade gapless blend fade trim silence' },
{ tab: 'lyrics', titleKey: 'settings.lyricsSourcesTitle', keywords: 'lyrics sources providers lrclib netease server youlyplus karaoke standard static' }, { tab: 'lyrics', titleKey: 'settings.lyricsSourcesTitle', keywords: 'lyrics sources providers lrclib netease server youlyplus karaoke standard static' },
{ tab: 'lyrics', titleKey: 'settings.sidebarLyricsStyle', keywords: 'lyrics scroll style classic apple music' }, { tab: 'lyrics', titleKey: 'settings.sidebarLyricsStyle', keywords: 'lyrics scroll style classic apple music' },
{ tab: 'integrations', titleKey: 'musicNetwork.title', keywords: 'last.fm lastfm libre.fm rocksky listenbrainz maloja scrobble scrobbling music network' }, { tab: 'integrations', titleKey: 'musicNetwork.title', keywords: 'last.fm lastfm libre.fm rocksky listenbrainz maloja scrobble scrobbling music network' },
@@ -47,7 +48,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'personalisation',titleKey: 'settings.sidebarTitle', keywords: 'sidebar nav navigation items reorder customize' }, { tab: 'personalisation',titleKey: 'settings.sidebarTitle', keywords: 'sidebar nav navigation items reorder customize' },
{ tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' }, { tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' },
{ tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'mainstage home page customize sections' }, { tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'mainstage home page customize sections' },
{ tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' }, { tab: 'personalisation',titleKey: 'settings.queueSettingsTitle', keywords: 'queue settings display mode list playlist timeline toolbar buttons reorder customize shuffle save load behaviour behavior preserve play next order' },
{ tab: 'personalisation',titleKey: 'settings.playlistLayoutTitle', keywords: 'playlist page layout add songs import csv download zip cache offline suggestions controls hide show' }, { tab: 'personalisation',titleKey: 'settings.playlistLayoutTitle', keywords: 'playlist page layout add songs import csv download zip cache offline suggestions controls hide show' },
{ tab: 'personalisation',titleKey: 'settings.playerBarTitle', keywords: 'player bar playback favorites stars rating lastfm love equalizer mini player controls hide show' }, { tab: 'personalisation',titleKey: 'settings.playerBarTitle', keywords: 'player bar playback favorites stars rating lastfm love equalizer mini player controls hide show' },
{ tab: 'appearance', titleKey: 'settings.libraryGridMaxColumnsTitle', keywords: 'grid columns album artist playlist cards layout appearance performance scroll paint' }, { tab: 'appearance', titleKey: 'settings.libraryGridMaxColumnsTitle', keywords: 'grid columns album artist playlist cards layout appearance performance scroll paint' },
+1
View File
@@ -375,6 +375,7 @@ const CONTRIBUTOR_ENTRIES = [
'Queue: switchable display mode — Queue (upcoming only) vs Playlist (full list), with header toggle and settings entry (PR #922)', 'Queue: switchable display mode — Queue (upcoming only) vs Playlist (full list), with header toggle and settings entry (PR #922)',
'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, free-form community themes (safety floor + state-reactive styling), and per-theme download counts with popularity/newest/name sorting; 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015, #1016, #1018, #1020, #1036)', 'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, free-form community themes (safety floor + state-reactive styling), and per-theme download counts with popularity/newest/name sorting; 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015, #1016, #1018, #1020, #1036)',
'AutoDJ as a standalone playback mode — own toolbar button and audio-settings entry, mutually exclusive with Crossfade and Gapless via a shared transition helper, combined playlist save/load submenu, and boxed playback settings sections (PR #1124)', 'AutoDJ as a standalone playback mode — own toolbar button and audio-settings entry, mutually exclusive with Crossfade and Gapless via a shared transition helper, combined playlist save/load submenu, and boxed playback settings sections (PR #1124)',
'Settings refactor — reusable boxed panels (SettingsGroup/SettingsToggle) rolled across every tab, Normalization and Track transitions promoted to their own Audio categories, a unified Queue Settings group on Personalisation, and the Audio Output Device category gated out on macOS (PR #1126, #1130)',
], ],
}, },
{ {
+1 -1
View File
@@ -24,7 +24,7 @@ interface UseAudioDevicesProbeResult {
* (`audio:device-changed` / `audio:device-reset`). * (`audio:device-changed` / `audio:device-reset`).
* *
* macOS short-circuits the audio stream is pinned to the system default * macOS short-circuits the audio stream is pinned to the system default
* there (see `audioOutputDeviceMacNotice`) and the picker UI is hidden. * there, and the whole output-device category is gated out in settings.
*/ */
export function useAudioDevicesProbe(t: TFunction): UseAudioDevicesProbeResult { export function useAudioDevicesProbe(t: TFunction): UseAudioDevicesProbeResult {
const [audioDevices, setAudioDevices] = useState<string[]>([]); const [audioDevices, setAudioDevices] = useState<string[]>([]);
+1 -1
View File
@@ -209,7 +209,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'aktuelle Systemausgabe', audioOutputDeviceOsDefaultNow: 'aktuelle Systemausgabe',
audioOutputDeviceListError: 'Audiogeräteliste konnte nicht geladen werden.', audioOutputDeviceListError: 'Audiogeräteliste konnte nicht geladen werden.',
audioOutputDeviceNotInCurrentList: 'nicht in der aktuellen Liste', audioOutputDeviceNotInCurrentList: 'nicht in der aktuellen Liste',
audioOutputDeviceMacNotice: 'Auf macOS folgt die Wiedergabe aus technischen Gründen zurzeit immer dem System-Ausgabegerät. Du kannst das Ziel über Systemeinstellungen → Ton oder das Lautsprecher-Symbol in der Menüleiste wechseln. Hintergrund: CoreAudio löst beim Öffnen eines nicht-Default-Streams eine Mikrofonberechtigungsabfrage aus — wir vermeiden sie, indem wir stets den System-Default verwenden.',
hiResTitle: 'Native Hi-Res-Wiedergabe', hiResTitle: 'Native Hi-Res-Wiedergabe',
hiResEnabled: 'Native Hi-Res-Wiedergabe aktivieren', hiResEnabled: 'Native Hi-Res-Wiedergabe aktivieren',
hiResDesc: "Spielt jeden Titel mit seiner ursprünglichen Abtastrate ab, statt alles auf 44,1 kHz herunterzurechnen, und stellt das Ausgabegerät passend zur Datei um (88,2 kHz und höher). Nur aktivieren, wenn Hardware und Netzwerk hohe Abtastraten zuverlässig verarbeiten.", hiResDesc: "Spielt jeden Titel mit seiner ursprünglichen Abtastrate ab, statt alles auf 44,1 kHz herunterzurechnen, und stellt das Ausgabegerät passend zur Datei um (88,2 kHz und höher). Nur aktivieren, wenn Hardware und Netzwerk hohe Abtastraten zuverlässig verarbeiten.",
@@ -421,6 +420,7 @@ export const settings = {
integrationsPrivacyTitle: 'Hinweis zum Datenschutz', integrationsPrivacyTitle: 'Hinweis zum Datenschutz',
integrationsPrivacyBody: 'Alle Integrationen auf diesem Tab sind <strong>Opt-in</strong> und senden, sobald aktiviert, Daten an externe Dienste oder an deinen Navidrome-Server. Last.fm erhält deinen Wiedergabeverlauf, Discord zeigt den aktuell laufenden Track in deinem Profil an, Bandsintown wird pro Künstler für Tour-Daten abgefragt, und der „Jetzt läuft"-Hinweis veröffentlicht deinen aktuellen Titel für andere Benutzer deines Navidrome-Servers. Wer das nicht möchte, lässt den jeweiligen Abschnitt einfach deaktiviert.', integrationsPrivacyBody: 'Alle Integrationen auf diesem Tab sind <strong>Opt-in</strong> und senden, sobald aktiviert, Daten an externe Dienste oder an deinen Navidrome-Server. Last.fm erhält deinen Wiedergabeverlauf, Discord zeigt den aktuell laufenden Track in deinem Profil an, Bandsintown wird pro Künstler für Tour-Daten abgefragt, und der „Jetzt läuft"-Hinweis veröffentlicht deinen aktuellen Titel für andere Benutzer deines Navidrome-Servers. Wer das nicht möchte, lässt den jeweiligen Abschnitt einfach deaktiviert.',
homeCustomizerTitle: 'Mainstage', homeCustomizerTitle: 'Mainstage',
queueSettingsTitle: 'Warteschlangen-Einstellungen',
queueModeTitle: 'Warteschlangen-Ansicht', queueModeTitle: 'Warteschlangen-Ansicht',
queueModeQueueSub: 'Zeigt nur kommende Titel. Der laufende Titel bleibt im Kopf und verschwindet aus der Liste, sobald er gespielt wurde.', queueModeQueueSub: 'Zeigt nur kommende Titel. Der laufende Titel bleibt im Kopf und verschwindet aus der Liste, sobald er gespielt wurde.',
queueModePlaylistSub: 'Behält die ganze Warteschlange in der Liste, der laufende Titel oben hervorgehoben; gespielte Titel bleiben stehen.', queueModePlaylistSub: 'Behält die ganze Warteschlange in der Liste, der laufende Titel oben hervorgehoben; gespielte Titel bleiben stehen.',
+1 -1
View File
@@ -212,7 +212,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'current system output', audioOutputDeviceOsDefaultNow: 'current system output',
audioOutputDeviceListError: 'Could not load the audio device list.', audioOutputDeviceListError: 'Could not load the audio device list.',
audioOutputDeviceNotInCurrentList: 'not in current list', audioOutputDeviceNotInCurrentList: 'not in current list',
audioOutputDeviceMacNotice: 'On macOS, playback currently always follows the system output device for technical reasons. Change the target via System Settings → Sound or the speaker icon in the menu bar. Background: CoreAudio triggers a microphone-permission prompt when opening a non-default stream — we avoid it by always using the system default.',
hiResTitle: 'Native Hi-Res Playback', hiResTitle: 'Native Hi-Res Playback',
hiResEnabled: 'Enable native hi-res playback', hiResEnabled: 'Enable native hi-res playback',
hiResDesc: "Plays each track at its original sample rate instead of resampling everything to 44.1 kHz, switching the audio device to match the file (88.2 kHz and up). Enable only if your hardware and network reliably handle high sample rates.", hiResDesc: "Plays each track at its original sample rate instead of resampling everything to 44.1 kHz, switching the audio device to match the file (88.2 kHz and up). Enable only if your hardware and network reliably handle high sample rates.",
@@ -465,6 +464,7 @@ export const settings = {
integrationsPrivacyTitle: 'Privacy notice', integrationsPrivacyTitle: 'Privacy notice',
integrationsPrivacyBody: 'All integrations on this tab are <strong>opt-in</strong> and send data to external services or to your Navidrome server when enabled. Last.fm receives your listening history, Discord shows the currently playing track in your profile, Bandsintown is queried per artist to fetch tour dates, and the Now-Playing share publishes your current track to other users of your Navidrome server. If you don\'t want any of this, simply leave the corresponding section disabled.', integrationsPrivacyBody: 'All integrations on this tab are <strong>opt-in</strong> and send data to external services or to your Navidrome server when enabled. Last.fm receives your listening history, Discord shows the currently playing track in your profile, Bandsintown is queried per artist to fetch tour dates, and the Now-Playing share publishes your current track to other users of your Navidrome server. If you don\'t want any of this, simply leave the corresponding section disabled.',
homeCustomizerTitle: 'Mainstage', homeCustomizerTitle: 'Mainstage',
queueSettingsTitle: 'Queue Settings',
queueModeTitle: 'Queue Display Mode', queueModeTitle: 'Queue Display Mode',
queueModeQueueSub: 'Show only upcoming tracks. The current track stays in the header and leaves the list once it has played.', queueModeQueueSub: 'Show only upcoming tracks. The current track stays in the header and leaves the list once it has played.',
queueModePlaylistSub: 'Keep the whole queue in the list with the current track highlighted at the top; played tracks stay.', queueModePlaylistSub: 'Keep the whole queue in the list with the current track highlighted at the top; played tracks stay.',
+1 -1
View File
@@ -208,7 +208,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'salida actual del sistema', audioOutputDeviceOsDefaultNow: 'salida actual del sistema',
audioOutputDeviceListError: 'No se pudo cargar la lista de dispositivos de audio.', audioOutputDeviceListError: 'No se pudo cargar la lista de dispositivos de audio.',
audioOutputDeviceNotInCurrentList: 'no está en la lista actual', audioOutputDeviceNotInCurrentList: 'no está en la lista actual',
audioOutputDeviceMacNotice: 'En macOS, la reproducción actualmente sigue siempre al dispositivo de salida del sistema por razones técnicas. Cambia el destino mediante Ajustes del Sistema → Sonido o el icono del altavoz en la barra de menús. Motivo: CoreAudio activa una solicitud de permiso de micrófono al abrir un stream no-predeterminado — lo evitamos usando siempre la salida por defecto del sistema.',
hiResTitle: 'Reproducción Nativa Hi-Res', hiResTitle: 'Reproducción Nativa Hi-Res',
hiResEnabled: 'Habilitar reproducción nativa hi-res', hiResEnabled: 'Habilitar reproducción nativa hi-res',
hiResDesc: "Reproduce cada pista a su frecuencia de muestreo original en vez de remuestrear todo a 44.1 kHz, ajustando el dispositivo de salida para que coincida con el archivo (88.2 kHz o más). Habilítalo solo si tu hardware y tu red soportan de forma fiable altas tasas de muestreo.", hiResDesc: "Reproduce cada pista a su frecuencia de muestreo original en vez de remuestrear todo a 44.1 kHz, ajustando el dispositivo de salida para que coincida con el archivo (88.2 kHz o más). Habilítalo solo si tu hardware y tu red soportan de forma fiable altas tasas de muestreo.",
@@ -420,6 +419,7 @@ export const settings = {
integrationsPrivacyTitle: 'Aviso de privacidad', integrationsPrivacyTitle: 'Aviso de privacidad',
integrationsPrivacyBody: 'Todas las integraciones de esta pestaña son <strong>opt-in</strong> y, una vez activadas, envían datos a servicios externos o a tu servidor Navidrome. Last.fm recibe tu historial de escucha, Discord muestra la pista actual en tu perfil, Bandsintown se consulta por artista para obtener fechas de gira, y la compartición «En reproducción» publica tu pista actual para otros usuarios de tu servidor Navidrome. Si no quieres nada de esto, simplemente deja desactivada la sección correspondiente.', integrationsPrivacyBody: 'Todas las integraciones de esta pestaña son <strong>opt-in</strong> y, una vez activadas, envían datos a servicios externos o a tu servidor Navidrome. Last.fm recibe tu historial de escucha, Discord muestra la pista actual en tu perfil, Bandsintown se consulta por artista para obtener fechas de gira, y la compartición «En reproducción» publica tu pista actual para otros usuarios de tu servidor Navidrome. Si no quieres nada de esto, simplemente deja desactivada la sección correspondiente.',
homeCustomizerTitle: 'Escenario Principal', homeCustomizerTitle: 'Escenario Principal',
queueSettingsTitle: 'Ajustes de la cola',
queueModeTitle: 'Modo de visualización de la cola', queueModeTitle: 'Modo de visualización de la cola',
queueModeQueueSub: 'Muestra solo las próximas pistas. La pista actual permanece en el encabezado y sale de la lista al reproducirse.', queueModeQueueSub: 'Muestra solo las próximas pistas. La pista actual permanece en el encabezado y sale de la lista al reproducirse.',
queueModePlaylistSub: 'Mantiene toda la cola en la lista, con la pista actual resaltada arriba; las reproducidas permanecen.', queueModePlaylistSub: 'Mantiene toda la cola en la lista, con la pista actual resaltada arriba; las reproducidas permanecen.',
+1 -1
View File
@@ -208,7 +208,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'sortie système actuelle', audioOutputDeviceOsDefaultNow: 'sortie système actuelle',
audioOutputDeviceListError: 'Impossible de charger la liste des périphériques audio.', audioOutputDeviceListError: 'Impossible de charger la liste des périphériques audio.',
audioOutputDeviceNotInCurrentList: 'absent de la liste actuelle', audioOutputDeviceNotInCurrentList: 'absent de la liste actuelle',
audioOutputDeviceMacNotice: 'Sur macOS, la lecture suit actuellement toujours la sortie audio du système pour des raisons techniques. Changez la cible via Réglages Système → Son ou l\'icône haut-parleur de la barre de menus. Contexte : CoreAudio déclenche une demande d\'autorisation du microphone à l\'ouverture d\'un flux non par défaut — nous l\'évitons en utilisant toujours la sortie système par défaut.',
hiResTitle: 'Lecture haute résolution native', hiResTitle: 'Lecture haute résolution native',
hiResEnabled: 'Activer la lecture haute résolution native', hiResEnabled: 'Activer la lecture haute résolution native',
hiResDesc: "Lit chaque piste à sa fréquence d'échantillonnage d'origine au lieu de tout rééchantillonner à 44,1 kHz, en réglant le périphérique de sortie sur celle du fichier (88,2 kHz et plus). N'activer que si le matériel et le réseau gèrent ces hautes fréquences de façon fiable.", hiResDesc: "Lit chaque piste à sa fréquence d'échantillonnage d'origine au lieu de tout rééchantillonner à 44,1 kHz, en réglant le périphérique de sortie sur celle du fichier (88,2 kHz et plus). N'activer que si le matériel et le réseau gèrent ces hautes fréquences de façon fiable.",
@@ -408,6 +407,7 @@ export const settings = {
integrationsPrivacyTitle: 'Avis de confidentialité', integrationsPrivacyTitle: 'Avis de confidentialité',
integrationsPrivacyBody: 'Toutes les intégrations de cet onglet sont <strong>facultatives</strong> et, une fois activées, envoient des données à des services externes ou à votre serveur Navidrome. Last.fm reçoit votre historique d\'écoute, Discord affiche le morceau en cours dans votre profil, Bandsintown est interrogé par artiste pour récupérer les dates de tournée, et le partage « En cours de lecture » publie votre morceau actuel auprès des autres utilisateurs de votre serveur Navidrome. Si vous ne voulez rien de tout cela, laissez simplement la section correspondante désactivée.', integrationsPrivacyBody: 'Toutes les intégrations de cet onglet sont <strong>facultatives</strong> et, une fois activées, envoient des données à des services externes ou à votre serveur Navidrome. Last.fm reçoit votre historique d\'écoute, Discord affiche le morceau en cours dans votre profil, Bandsintown est interrogé par artiste pour récupérer les dates de tournée, et le partage « En cours de lecture » publie votre morceau actuel auprès des autres utilisateurs de votre serveur Navidrome. Si vous ne voulez rien de tout cela, laissez simplement la section correspondante désactivée.',
homeCustomizerTitle: 'Scène principale', homeCustomizerTitle: 'Scène principale',
queueSettingsTitle: 'Paramètres de la file d\'attente',
queueModeTitle: 'Mode d\'affichage de la file', queueModeTitle: 'Mode d\'affichage de la file',
queueModeQueueSub: 'N\'affiche que les pistes à venir. La piste en cours reste dans l\'en-tête et quitte la liste une fois jouée.', queueModeQueueSub: 'N\'affiche que les pistes à venir. La piste en cours reste dans l\'en-tête et quitte la liste une fois jouée.',
queueModePlaylistSub: 'Conserve toute la file dans la liste, la piste en cours surlignée en haut ; les pistes jouées restent.', queueModePlaylistSub: 'Conserve toute la file dans la liste, la piste en cours surlignée en haut ; les pistes jouées restent.',
+1 -1
View File
@@ -209,7 +209,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'gjeldende systemutgang', audioOutputDeviceOsDefaultNow: 'gjeldende systemutgang',
audioOutputDeviceListError: 'Kunne ikke laste listen over lydenheter.', audioOutputDeviceListError: 'Kunne ikke laste listen over lydenheter.',
audioOutputDeviceNotInCurrentList: 'ikke i gjeldende liste', audioOutputDeviceNotInCurrentList: 'ikke i gjeldende liste',
audioOutputDeviceMacNotice: 'På macOS følger avspillingen av tekniske årsaker alltid systemets lydutgang. Endre målet via Systeminnstillinger → Lyd eller høyttalerikonet i menylinjen. Bakgrunn: CoreAudio utløser en mikrofontillatelsesdialog når en ikke-standard strøm åpnes — vi unngår det ved alltid å bruke systemets standardutgang.',
hiResTitle: 'Innebygd hi-res-avspilling', hiResTitle: 'Innebygd hi-res-avspilling',
hiResEnabled: 'Aktiver innebygd hi-res-avspilling', hiResEnabled: 'Aktiver innebygd hi-res-avspilling',
hiResDesc: "Spiller hvert spor med sin opprinnelige samplingsrate i stedet for å nedsample alt til 44,1 kHz, og stiller utdataenheten etter filen (88,2 kHz og høyere). Aktiver kun hvis maskinvare og nettverk håndterer høye samplingsrater pålitelig.", hiResDesc: "Spiller hvert spor med sin opprinnelige samplingsrate i stedet for å nedsample alt til 44,1 kHz, og stiller utdataenheten etter filen (88,2 kHz og høyere). Aktiver kun hvis maskinvare og nettverk håndterer høye samplingsrater pålitelig.",
@@ -407,6 +406,7 @@ export const settings = {
integrationsPrivacyTitle: 'Personvern-merknad', integrationsPrivacyTitle: 'Personvern-merknad',
integrationsPrivacyBody: 'Alle integrasjoner på denne fanen er <strong>frivillige</strong> og sender, når aktivert, data til eksterne tjenester eller til Navidrome-serveren din. Last.fm mottar lyttehistorikken din, Discord viser gjeldende spor i profilen din, Bandsintown spørres per artist for turnédatoer, og "Spilles nå"-delingen publiserer gjeldende spor til andre brukere av Navidrome-serveren din. Hvis du ikke ønsker noe av dette, la den aktuelle seksjonen stå deaktivert.', integrationsPrivacyBody: 'Alle integrasjoner på denne fanen er <strong>frivillige</strong> og sender, når aktivert, data til eksterne tjenester eller til Navidrome-serveren din. Last.fm mottar lyttehistorikken din, Discord viser gjeldende spor i profilen din, Bandsintown spørres per artist for turnédatoer, og "Spilles nå"-delingen publiserer gjeldende spor til andre brukere av Navidrome-serveren din. Hvis du ikke ønsker noe av dette, la den aktuelle seksjonen stå deaktivert.',
homeCustomizerTitle: 'Hovedscene', homeCustomizerTitle: 'Hovedscene',
queueSettingsTitle: 'Køinnstillinger',
queueModeTitle: 'Visningsmodus for kø', queueModeTitle: 'Visningsmodus for kø',
queueModeQueueSub: 'Viser bare kommende spor. Det gjeldende sporet blir i toppen og forsvinner fra listen når det er spilt.', queueModeQueueSub: 'Viser bare kommende spor. Det gjeldende sporet blir i toppen og forsvinner fra listen når det er spilt.',
queueModePlaylistSub: 'Beholder hele køen i listen med gjeldende spor uthevet øverst; avspilte spor blir værende.', queueModePlaylistSub: 'Beholder hele køen i listen med gjeldende spor uthevet øverst; avspilte spor blir værende.',
+1 -1
View File
@@ -208,7 +208,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'huidige systeemuitvoer', audioOutputDeviceOsDefaultNow: 'huidige systeemuitvoer',
audioOutputDeviceListError: 'De lijst met audio-apparaten kon niet worden geladen.', audioOutputDeviceListError: 'De lijst met audio-apparaten kon niet worden geladen.',
audioOutputDeviceNotInCurrentList: 'staat niet in de huidige lijst', audioOutputDeviceNotInCurrentList: 'staat niet in de huidige lijst',
audioOutputDeviceMacNotice: 'Op macOS volgt de weergave om technische redenen altijd het standaard-uitvoerapparaat van het systeem. Wijzig het doel via Systeeminstellingen → Geluid of via het luidsprekerpictogram in de menubalk. Achtergrond: CoreAudio vraagt bij het openen van een niet-standaard stream om microfoontoestemming — dat vermijden we door altijd de systeemstandaard te gebruiken.',
hiResTitle: 'Natieve hi-res-weergave', hiResTitle: 'Natieve hi-res-weergave',
hiResEnabled: 'Natieve hi-res-weergave inschakelen', hiResEnabled: 'Natieve hi-res-weergave inschakelen',
hiResDesc: "Speelt elke track af op zijn oorspronkelijke samplerate in plaats van alles te herbemonsteren naar 44,1 kHz, en stelt het uitvoerapparaat af op het bestand (88,2 kHz en hoger). Alleen inschakelen als hardware en netwerk hoge samplerates betrouwbaar aankunnen.", hiResDesc: "Speelt elke track af op zijn oorspronkelijke samplerate in plaats van alles te herbemonsteren naar 44,1 kHz, en stelt het uitvoerapparaat af op het bestand (88,2 kHz en hoger). Alleen inschakelen als hardware en netwerk hoge samplerates betrouwbaar aankunnen.",
@@ -408,6 +407,7 @@ export const settings = {
integrationsPrivacyTitle: 'Privacyverklaring', integrationsPrivacyTitle: 'Privacyverklaring',
integrationsPrivacyBody: 'Alle integraties op dit tabblad zijn <strong>opt-in</strong> en sturen, eenmaal ingeschakeld, gegevens naar externe diensten of naar je Navidrome-server. Last.fm ontvangt je luistergeschiedenis, Discord toont het nu spelende nummer in je profiel, Bandsintown wordt per artiest bevraagd voor tourdatums, en de "Nu aan het afspelen"-deling publiceert je huidige nummer bij andere gebruikers van je Navidrome-server. Wil je dit niet, laat dan de bijbehorende sectie gewoon uitgeschakeld.', integrationsPrivacyBody: 'Alle integraties op dit tabblad zijn <strong>opt-in</strong> en sturen, eenmaal ingeschakeld, gegevens naar externe diensten of naar je Navidrome-server. Last.fm ontvangt je luistergeschiedenis, Discord toont het nu spelende nummer in je profiel, Bandsintown wordt per artiest bevraagd voor tourdatums, en de "Nu aan het afspelen"-deling publiceert je huidige nummer bij andere gebruikers van je Navidrome-server. Wil je dit niet, laat dan de bijbehorende sectie gewoon uitgeschakeld.',
homeCustomizerTitle: 'Hoofdpodium', homeCustomizerTitle: 'Hoofdpodium',
queueSettingsTitle: 'Wachtrij-instellingen',
queueModeTitle: 'Weergavemodus wachtrij', queueModeTitle: 'Weergavemodus wachtrij',
queueModeQueueSub: 'Toont alleen komende nummers. Het huidige nummer blijft in de kop en verdwijnt uit de lijst zodra het is afgespeeld.', queueModeQueueSub: 'Toont alleen komende nummers. Het huidige nummer blijft in de kop en verdwijnt uit de lijst zodra het is afgespeeld.',
queueModePlaylistSub: 'Houdt de hele wachtrij in de lijst met het huidige nummer bovenaan gemarkeerd; afgespeelde nummers blijven staan.', queueModePlaylistSub: 'Houdt de hele wachtrij in de lijst met het huidige nummer bovenaan gemarkeerd; afgespeelde nummers blijven staan.',
+1 -1
View File
@@ -211,7 +211,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'ieșirea curentă a sistemului', audioOutputDeviceOsDefaultNow: 'ieșirea curentă a sistemului',
audioOutputDeviceListError: 'Nu s-a putut încărca lista dispozitivelor audio', audioOutputDeviceListError: 'Nu s-a putut încărca lista dispozitivelor audio',
audioOutputDeviceNotInCurrentList: 'nu există în lista curentă', audioOutputDeviceNotInCurrentList: 'nu există în lista curentă',
audioOutputDeviceMacNotice: 'În macOS, redarea urmărește mereu dispozitivul audio de ieșire al sistemului din motive tehnice. Schimbă ținta în Setări → Sunet sau iconița difuzor din bara de meniu. Fundal: CoreAudio declanșează un prompt de permisiune de microfon când se deschide un stream non-implicit — îl evităm folosind opțiunea implicită a sistemului mereu.',
hiResTitle: 'Playback Hi-Res nativ', hiResTitle: 'Playback Hi-Res nativ',
hiResEnabled: 'Pornește playback-ul hi-res nativ', hiResEnabled: 'Pornește playback-ul hi-res nativ',
hiResDesc: "Redă fiecare piesă la rata sa de eșantionare originală în loc să reeșantioneze totul la 44.1 kHz, comutând dispozitivul de ieșire pentru a se potrivi cu fișierul (88.2 kHz și peste). Pornește doar dacă hardware-ul și rețeaua gestionează fiabil rate mari de eșantionare.", hiResDesc: "Redă fiecare piesă la rata sa de eșantionare originală în loc să reeșantioneze totul la 44.1 kHz, comutând dispozitivul de ieșire pentru a se potrivi cu fișierul (88.2 kHz și peste). Pornește doar dacă hardware-ul și rețeaua gestionează fiabil rate mari de eșantionare.",
@@ -423,6 +422,7 @@ export const settings = {
integrationsPrivacyTitle: 'Notificare de confidențialitate', integrationsPrivacyTitle: 'Notificare de confidențialitate',
integrationsPrivacyBody: 'Toate integrările din acest tab sunt <strong>opționale</strong> și trimit date către servicii externe sau server-ul tău Navidrome când sunt activate. Last.fm primește istoricul ascultărilor tale, Discord arată piesa redată curent în profilul tău, Bandsintown e interogat per artist pentru a aduce date de turnee, și distribuirea Now-Playing publică piesa curentă către alți useri din server-ul tău Navidrome. Dacă nu vrei vreo opțiune, doar lasă secțiunea corespunzătoare dezactivată', integrationsPrivacyBody: 'Toate integrările din acest tab sunt <strong>opționale</strong> și trimit date către servicii externe sau server-ul tău Navidrome când sunt activate. Last.fm primește istoricul ascultărilor tale, Discord arată piesa redată curent în profilul tău, Bandsintown e interogat per artist pentru a aduce date de turnee, și distribuirea Now-Playing publică piesa curentă către alți useri din server-ul tău Navidrome. Dacă nu vrei vreo opțiune, doar lasă secțiunea corespunzătoare dezactivată',
homeCustomizerTitle: 'Scena Principală', homeCustomizerTitle: 'Scena Principală',
queueSettingsTitle: 'Setări coadă',
queueModeTitle: 'Mod de afișare a cozii', queueModeTitle: 'Mod de afișare a cozii',
queueModeQueueSub: 'Arată doar piesele următoare. Piesa curentă rămâne în antet și iese din listă după ce a fost redată.', queueModeQueueSub: 'Arată doar piesele următoare. Piesa curentă rămâne în antet și iese din listă după ce a fost redată.',
queueModePlaylistSub: 'Păstrează toată coada în listă, cu piesa curentă evidențiată sus; piesele redate rămân.', queueModePlaylistSub: 'Păstrează toată coada în listă, cu piesa curentă evidențiată sus; piesele redate rămân.',
+1 -1
View File
@@ -212,7 +212,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: 'текущий системный вывод', audioOutputDeviceOsDefaultNow: 'текущий системный вывод',
audioOutputDeviceListError: 'Не удалось загрузить список аудиоустройств.', audioOutputDeviceListError: 'Не удалось загрузить список аудиоустройств.',
audioOutputDeviceNotInCurrentList: 'нет в текущем списке', audioOutputDeviceNotInCurrentList: 'нет в текущем списке',
audioOutputDeviceMacNotice: 'В macOS воспроизведение в настоящее время всегда следует за системным устройством вывода по техническим причинам. Измените цель через Системные настройки → Звук или значок динамика в строке меню. Причина: CoreAudio запрашивает разрешение на использование микрофона при открытии не-стандартного потока — мы избегаем этого, всегда используя системный вывод по умолчанию.',
hiResTitle: 'Нативное воспроизведение Hi-Res', hiResTitle: 'Нативное воспроизведение Hi-Res',
hiResEnabled: 'Включить нативное Hi-Res воспроизведение', hiResEnabled: 'Включить нативное Hi-Res воспроизведение',
hiResDesc: "Воспроизводит каждый трек с его исходной частотой дискретизации вместо передискретизации всего в 44,1 кГц, переключая устройство вывода под частоту файла (88,2 кГц и выше). Включайте только если оборудование и сеть надёжно справляются с высокими частотами.", hiResDesc: "Воспроизводит каждый трек с его исходной частотой дискретизации вместо передискретизации всего в 44,1 кГц, переключая устройство вывода под частоту файла (88,2 кГц и выше). Включайте только если оборудование и сеть надёжно справляются с высокими частотами.",
@@ -478,6 +477,7 @@ export const settings = {
integrationsPrivacyTitle: 'Уведомление о конфиденциальности', integrationsPrivacyTitle: 'Уведомление о конфиденциальности',
integrationsPrivacyBody: 'Все интеграции на этой вкладке включаются <strong>по желанию</strong> и при активации отправляют данные во внешние сервисы или на ваш сервер Navidrome. Last.fm получает вашу историю прослушивания, Discord показывает текущую композицию в вашем профиле, Bandsintown опрашивается по артисту для получения дат гастролей, а «Сейчас играет» делится текущей композицией с другими пользователями вашего сервера Navidrome. Если вы не хотите ничего из этого, просто оставьте соответствующий раздел отключённым.', integrationsPrivacyBody: 'Все интеграции на этой вкладке включаются <strong>по желанию</strong> и при активации отправляют данные во внешние сервисы или на ваш сервер Navidrome. Last.fm получает вашу историю прослушивания, Discord показывает текущую композицию в вашем профиле, Bandsintown опрашивается по артисту для получения дат гастролей, а «Сейчас играет» делится текущей композицией с другими пользователями вашего сервера Navidrome. Если вы не хотите ничего из этого, просто оставьте соответствующий раздел отключённым.',
homeCustomizerTitle: 'Для вас', homeCustomizerTitle: 'Для вас',
queueSettingsTitle: 'Настройки очереди',
queueModeTitle: 'Режим отображения очереди', queueModeTitle: 'Режим отображения очереди',
queueModeQueueSub: 'Показывает только предстоящие треки. Текущий трек остаётся в заголовке и исчезает из списка после воспроизведения.', queueModeQueueSub: 'Показывает только предстоящие треки. Текущий трек остаётся в заголовке и исчезает из списка после воспроизведения.',
queueModePlaylistSub: 'Сохраняет всю очередь в списке, текущий трек выделен вверху; воспроизведённые треки остаются.', queueModePlaylistSub: 'Сохраняет всю очередь в списке, текущий трек выделен вверху; воспроизведённые треки остаются.',
+1 -1
View File
@@ -208,7 +208,6 @@ export const settings = {
audioOutputDeviceOsDefaultNow: '当前系统输出', audioOutputDeviceOsDefaultNow: '当前系统输出',
audioOutputDeviceListError: '无法加载音频设备列表。', audioOutputDeviceListError: '无法加载音频设备列表。',
audioOutputDeviceNotInCurrentList: '不在当前列表中', audioOutputDeviceNotInCurrentList: '不在当前列表中',
audioOutputDeviceMacNotice: '在 macOS 上,出于技术原因,目前播放始终跟随系统输出设备。请通过 系统设置 → 声音 或菜单栏扬声器图标切换目标设备。背景:打开非默认音频流时 CoreAudio 会触发麦克风权限提示 —— 我们通过始终使用系统默认输出来避免此提示。',
hiResTitle: '原生高清晰度播放', hiResTitle: '原生高清晰度播放',
hiResEnabled: '启用原生高清晰度播放', hiResEnabled: '启用原生高清晰度播放',
hiResDesc: "以每个曲目的原始采样率播放,而不是将所有内容重采样到 44.1 kHz,并将输出设备切换为与文件匹配(88.2 kHz 及以上)。仅在硬件和网络能可靠处理高采样率时启用。", hiResDesc: "以每个曲目的原始采样率播放,而不是将所有内容重采样到 44.1 kHz,并将输出设备切换为与文件匹配(88.2 kHz 及以上)。仅在硬件和网络能可靠处理高采样率时启用。",
@@ -407,6 +406,7 @@ export const settings = {
integrationsPrivacyTitle: '隐私说明', integrationsPrivacyTitle: '隐私说明',
integrationsPrivacyBody: '此标签页中的所有集成均为 <strong>选择加入</strong>,启用后会向外部服务或您的 Navidrome 服务器发送数据。Last.fm 接收您的收听历史,Discord 在您的个人资料中显示正在播放的曲目,Bandsintown 会按艺人查询以获取巡演日期,"正在播放" 分享会向您 Navidrome 服务器的其他用户公开您当前的曲目。如果您不需要这些功能,只需将相应部分保持停用即可。', integrationsPrivacyBody: '此标签页中的所有集成均为 <strong>选择加入</strong>,启用后会向外部服务或您的 Navidrome 服务器发送数据。Last.fm 接收您的收听历史,Discord 在您的个人资料中显示正在播放的曲目,Bandsintown 会按艺人查询以获取巡演日期,"正在播放" 分享会向您 Navidrome 服务器的其他用户公开您当前的曲目。如果您不需要这些功能,只需将相应部分保持停用即可。',
homeCustomizerTitle: '主舞台', homeCustomizerTitle: '主舞台',
queueSettingsTitle: '队列设置',
queueModeTitle: '队列显示模式', queueModeTitle: '队列显示模式',
queueModeQueueSub: '仅显示即将播放的曲目。当前曲目保留在标题栏中,播放完毕后从列表移除。', queueModeQueueSub: '仅显示即将播放的曲目。当前曲目保留在标题栏中,播放完毕后从列表移除。',
queueModePlaylistSub: '在列表中保留整个队列,当前曲目在顶部高亮显示;已播放的曲目保留。', queueModePlaylistSub: '在列表中保留整个队列,当前曲目在顶部高亮显示;已播放的曲目保留。',
+8
View File
@@ -587,6 +587,14 @@ span.settings-server-use-active-slot {
color: var(--accent); color: var(--accent);
} }
/* Right-aligned slot in the group title row holding the optional Advanced
badge and/or action (e.g. a reset button), in that order. */
.settings-group-title-end {
margin-left: auto;
display: inline-flex;
align-items: center;
}
.settings-group-desc { .settings-group-desc {
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);