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
+38 -23
View File
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
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 Equalizer from '../Equalizer';
import SettingsSubSection from '../SettingsSubSection';
@@ -8,11 +8,11 @@ import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../../utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '../../hooks/useAudioDevicesProbe';
import { IS_MACOS } from '../../utils/platform';
import { AudioOutputDeviceSection } from './audio/AudioOutputDeviceSection';
import { NormalizationBlock } from './audio/NormalizationBlock';
import { Gauge } from 'lucide-react';
import { PlaybackBehaviorBlock } from './audio/PlaybackBehaviorBlock';
import { PlaybackRateBlock } from './audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from './audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from './audio/TrackPreviewsSection';
export function AudioTab() {
@@ -37,15 +37,41 @@ export function AudioTab() {
return (
<>
<AudioOutputDeviceSection
audioDevices={audioDevices}
osDefaultAudioDeviceId={osDefaultAudioDeviceId}
deviceSwitching={deviceSwitching}
devicesLoading={devicesLoading}
setDeviceSwitching={setDeviceSwitching}
refreshAudioDevices={refreshAudioDevices}
t={t}
/>
{/* 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
audioDevices={audioDevices}
osDefaultAudioDeviceId={osDefaultAudioDeviceId}
deviceSwitching={deviceSwitching}
devicesLoading={devicesLoading}
setDeviceSwitching={setDeviceSwitching}
refreshAudioDevices={refreshAudioDevices}
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 */}
<SettingsSubSection
@@ -89,17 +115,6 @@ export function AudioTab() {
</div>
</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} />
</>
);
+40 -29
View File
@@ -1,5 +1,5 @@
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 { useAuthStore } from '../../store/authStore';
import { useHomeStore } from '../../store/homeStore';
@@ -21,6 +21,9 @@ export function PersonalisationTab() {
const { t } = useTranslation();
const queueDisplayMode = useAuthStore(s => s.queueDisplayMode);
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 (
<>
<SettingsSubSection
@@ -39,9 +42,7 @@ export function PersonalisationTab() {
</button>
}
>
<SettingsGroup>
<SidebarCustomizer />
</SettingsGroup>
<SidebarCustomizer />
</SettingsSubSection>
<SettingsSubSection
@@ -87,12 +88,14 @@ export function PersonalisationTab() {
</SettingsGroup>
</SettingsSubSection>
{/* Queue Settings — display mode, queue behaviour and (advanced) the
toolbar customizer, grouped under one category. */}
<SettingsSubSection
title={t('settings.queueModeTitle')}
title={t('settings.queueSettingsTitle')}
icon={<ListOrdered size={16} />}
>
<div className="settings-card">
<SettingsGroup>
<>
<SettingsGroup title={t('settings.queueModeTitle')}>
{/* Three mutually exclusive modes — exactly one is always active, so
turning one on turns the others off; the active one cannot be
switched off directly (ignore the uncheck). */}
@@ -117,29 +120,37 @@ export function PersonalisationTab() {
onChange={c => { if (c) setQueueDisplayMode('timeline'); }}
/>
</SettingsGroup>
</div>
</SettingsSubSection>
<SettingsSubSection
title={t('settings.queueToolbarTitle')}
icon={<ListMusic size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useQueueToolbarStore.getState().reset()}
data-tooltip={t('settings.queueToolbarReset')}
aria-label={t('settings.queueToolbarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<SettingsGroup>
<QueueToolbarCustomizer />
</SettingsGroup>
<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')}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useQueueToolbarStore.getState().reset()}
data-tooltip={t('settings.queueToolbarReset')}
aria-label={t('settings.queueToolbarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<QueueToolbarCustomizer />
</SettingsGroup>
)}
</>
</SettingsSubSection>
<SettingsSubSection
@@ -124,7 +124,11 @@ export function QueueToolbarCustomizer() {
{Icon ? (
<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>
<label className="toggle-switch" aria-label={label}>
+19 -1
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
/** Accent uppercase header. Omit for a plain boxed panel (no header) —
@@ -9,6 +10,12 @@ interface Props {
icon?: React.ReactNode;
/** Optional one-line description shown under the title. */
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;
}
@@ -18,13 +25,24 @@ interface Props {
* settings card. Wraps the `.settings-group` styles so the look stays
* 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 (
<div className="settings-group">
{title && (
<div className="settings-group-title">
{icon && <span className="settings-group-title-icon">{icon}</span>}
{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 className="settings-group-body">
+19 -15
View File
@@ -7,6 +7,7 @@ import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '.
import { useLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable';
import { ALL_NAV_ITEMS } from '../../config/navItems';
import { applySidebarDropReorder } from '../../utils/componentHelpers/sidebarNavReorder';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
type DropTarget = { idx: number; before: boolean; section: 'library' | 'system' } | null;
@@ -128,7 +129,7 @@ export function SidebarCustomizer() {
return (
<>
<div style={{ marginBottom: '1rem' }}>
<SettingsGroup>
<SettingsToggle
label={t('settings.randomNavSplitTitle')}
desc={t('settings.randomNavSplitDesc')}
@@ -148,22 +149,25 @@ export function SidebarCustomizer() {
checked={showLuckyMixMenu}
onChange={setShowLuckyMixMenu}
/>
</div>
<div ref={containerRef} onMouseMove={handleMouseMove} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* Library block */}
<div style={{ padding: '4px 0' }}>
<div className="sidebar-customizer-block-label">{t('sidebar.library')}</div>
{libraryItems.map((cfg, i) => renderRow(cfg, i, 'library'))}
</div>
{/* System block */}
<div style={{ padding: '4px 0' }}>
<div className="sidebar-customizer-block-label">{t('sidebar.system')}</div>
{systemItems.map((cfg, i) => renderRow(cfg, i, 'system'))}
<div className="sidebar-customizer-fixed-hint">
<span>{t('settings.sidebarFixed')}: {t('sidebar.nowPlaying')}, {t('sidebar.settings')}</span>
</SettingsGroup>
<SettingsGroup>
<div ref={containerRef} onMouseMove={handleMouseMove} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* Library block */}
<div style={{ padding: '4px 0' }}>
<div className="sidebar-customizer-block-label">{t('sidebar.library')}</div>
{libraryItems.map((cfg, i) => renderRow(cfg, i, 'library'))}
</div>
{/* System block */}
<div style={{ padding: '4px 0' }}>
<div className="sidebar-customizer-block-label">{t('sidebar.system')}</div>
{systemItems.map((cfg, i) => renderRow(cfg, i, 'system'))}
<div className="sidebar-customizer-fixed-hint">
<span>{t('settings.sidebarFixed')}: {t('sidebar.nowPlaying')}, {t('sidebar.settings')}</span>
</div>
</div>
</div>
</div>
</SettingsGroup>
</>
);
}
@@ -6,7 +6,6 @@ import CustomSelect from '../../CustomSelect';
import SettingsSubSection from '../../SettingsSubSection';
import { SettingsGroup } from '../SettingsGroup';
import { useAuthStore } from '../../../store/authStore';
import { IS_MACOS } from '../../../utils/platform';
import { buildAudioDeviceSelectOptions } from '../../../utils/audio/audioDeviceLabels';
interface Props {
@@ -20,8 +19,9 @@ interface Props {
}
/**
* Audio output device picker. macOS is hard-pinned to the system default,
* so the picker collapses to a notice on that platform.
* Audio output device picker. Not rendered on macOS — the audio stream is
* 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.
* device disappeared) we leave the previous selection in the store.
@@ -45,49 +45,41 @@ export function AudioOutputDeviceSection({
>
<div className="settings-card">
<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' }}>
{t('settings.audioOutputDeviceDesc')}
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<CustomSelect
style={{ flex: 1 }}
value={audioOutputDevice ?? ''}
disabled={deviceSwitching || devicesLoading}
onChange={async (val) => {
const device = val || null;
setDeviceSwitching(true);
try {
await invoke('audio_set_device', { deviceName: device });
setAudioOutputDevice(device);
} catch { /* device open failed — don't persist */ }
setDeviceSwitching(false);
}}
options={buildAudioDeviceSelectOptions(
audioDevices,
t('settings.audioOutputDeviceDefault'),
osDefaultAudioDeviceId,
t('settings.audioOutputDeviceOsDefaultNow'),
audioOutputDevice,
t('settings.audioOutputDeviceNotInCurrentList'),
)}
/>
<button
className="icon-btn"
onClick={() => refreshAudioDevices()}
disabled={devicesLoading || deviceSwitching}
data-tooltip={t('settings.audioOutputDeviceRefresh')}
>
<RotateCcw size={15} className={devicesLoading ? 'spin' : ''} />
</button>
</div>
</>
)}
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
{t('settings.audioOutputDeviceDesc')}
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<CustomSelect
style={{ flex: 1 }}
value={audioOutputDevice ?? ''}
disabled={deviceSwitching || devicesLoading}
onChange={async (val) => {
const device = val || null;
setDeviceSwitching(true);
try {
await invoke('audio_set_device', { deviceName: device });
setAudioOutputDevice(device);
} catch { /* device open failed — don't persist */ }
setDeviceSwitching(false);
}}
options={buildAudioDeviceSelectOptions(
audioDevices,
t('settings.audioOutputDeviceDefault'),
osDefaultAudioDeviceId,
t('settings.audioOutputDeviceOsDefaultNow'),
audioOutputDevice,
t('settings.audioOutputDeviceNotInCurrentList'),
)}
/>
<button
className="icon-btn"
onClick={() => refreshAudioDevices()}
disabled={devicesLoading || deviceSwitching}
data-tooltip={t('settings.audioOutputDeviceRefresh')}
>
<RotateCcw size={15} className={devicesLoading ? 'spin' : ''} />
</button>
</div>
</SettingsGroup>
</div>
</SettingsSubSection>
@@ -24,15 +24,16 @@ interface Props {
*
* Switching engines clears the other engine's enabled flag so only one
* 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) {
const auth = useAuthStore();
return (
<SettingsGroup
title={t('settings.normalization', { defaultValue: 'Normalization' })}
desc={t('settings.normalizationDesc')}
>
<SettingsGroup>
<div className="settings-segmented" style={{ marginBottom: auth.normalizationEngine === 'off' ? 0 : '0.85rem' }}>
<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.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.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.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' },
@@ -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.artistLayoutTitle', keywords: 'artist page layout sections order' },
{ 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.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' },