mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
ui(settings): restructure Normalization section for clarity and breathing room
The mode picker (Off / ReplayGain / LUFS) used to live in the right-hand action slot of a settings-toggle-row and the per-mode controls were stacked into the same parent with footer-style help text. Hard to scan, visually cramped, and odd compared to the rest of the Settings page. Refactor: - Mode picker becomes a full-width segmented row with even-flex buttons, using the new .settings-segmented utility. - Each mode renders its own .settings-norm-block sub-section with a subtle accent tint and border so the active configuration reads as one coherent group. - Inside the block, every setting is its own .settings-norm-field (control row + per-control help text immediately below). 1.1 rem gap between fields, 0.45 rem between row and help — clearly groups related text without crowding. - Sliders no longer max-cap at 200 px and instead flex to fill the row. - Inactive ghost buttons (Off, ReplayGain, RG mode, LUFS targets) get a visible border and a faint surface tint so they read as selectable slots in dark themes too. - LUFS mode gets a dedicated note-box explaining that brief volume drift on the very first play of a new track is the analysis pass at work, not a bug — subsequent plays use the cached measurement, and queued tracks are usually pre-analysed during the previous song. - "Trim before measurement (dB)" renamed to "Pre-analysis attenuation" (and equivalents in 8 locales). - New i18n keys: normalizationDesc, normalizationOff/ReplayGain/Lufs, loudnessTargetLufsDesc, loudnessFirstPlayNote, replayGainPreGainDesc, replayGainFallbackDesc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+136
-138
@@ -2272,150 +2272,148 @@ export default function Settings() {
|
||||
>
|
||||
<div className="settings-card">
|
||||
{/* Normalization */}
|
||||
<div className="settings-toggle-row">
|
||||
<div style={{ marginBottom: '0.6rem' }}>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.normalization', { defaultValue: 'Normalization' })}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<button
|
||||
className={`btn ${auth.normalizationEngine === 'off' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 10px' }}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(false);
|
||||
auth.setNormalizationEngine('off');
|
||||
}}
|
||||
>
|
||||
Off
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.normalizationEngine === 'replaygain' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 10px' }}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(true);
|
||||
auth.setNormalizationEngine('replaygain');
|
||||
}}
|
||||
>
|
||||
RG
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.normalizationEngine === 'loudness' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 10px' }}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(false);
|
||||
if (auth.normalizationEngine !== 'loudness') auth.setLoudnessTargetLufs(-12);
|
||||
auth.setNormalizationEngine('loudness');
|
||||
}}
|
||||
>
|
||||
LUFS
|
||||
</button>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2 }}>
|
||||
{t('settings.normalizationDesc')}
|
||||
</div>
|
||||
</div>
|
||||
{auth.normalizationEngine === 'loudness' && (
|
||||
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.55rem' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 140 }}>
|
||||
{t('settings.loudnessTargetLufs')}
|
||||
</span>
|
||||
<LoudnessLufsButtonGroup value={auth.loudnessTargetLufs} onSelect={auth.setLoudnessTargetLufs} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 140 }}>
|
||||
{t('settings.loudnessPreAnalysisAttenuation')}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={-24}
|
||||
max={0}
|
||||
step={0.5}
|
||||
value={auth.loudnessPreAnalysisAttenuationDb}
|
||||
onChange={e => auth.setLoudnessPreAnalysisAttenuationDb(Number(e.target.value))}
|
||||
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)', minWidth: 44, textAlign: 'right' }}>
|
||||
{auth.loudnessPreAnalysisAttenuationDb} dB
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
style={{ flexShrink: 0 }}
|
||||
disabled={
|
||||
auth.loudnessPreAnalysisAttenuationDb === DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB
|
||||
}
|
||||
onClick={() => auth.resetLoudnessPreAnalysisAttenuationDbDefault()}
|
||||
data-tooltip={t('settings.loudnessPreAnalysisAttenuationReset')}
|
||||
aria-label={t('settings.loudnessPreAnalysisAttenuationReset')}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.35, maxWidth: 520 }}>
|
||||
{t('settings.loudnessPreAnalysisAttenuationDesc')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{auth.normalizationEngine !== 'off' && (
|
||||
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
||||
{auth.normalizationEngine === 'replaygain' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{t('settings.replayGainMode')}:</span>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'auto' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||
onClick={() => auth.setReplayGainMode('auto')}
|
||||
>
|
||||
{t('settings.replayGainAuto')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'track' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||
onClick={() => auth.setReplayGainMode('track')}
|
||||
>
|
||||
{t('settings.replayGainTrack')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'album' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||
onClick={() => auth.setReplayGainMode('album')}
|
||||
>
|
||||
{t('settings.replayGainAlbum')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{auth.normalizationEngine === 'replaygain' && auth.replayGainMode === 'auto' && (
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
{t('settings.replayGainAutoDesc')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-segmented" style={{ marginBottom: auth.normalizationEngine === 'off' ? 0 : '0.85rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${auth.normalizationEngine === 'off' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(false);
|
||||
auth.setNormalizationEngine('off');
|
||||
}}
|
||||
>
|
||||
{t('settings.normalizationOff')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${auth.normalizationEngine === 'replaygain' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(true);
|
||||
auth.setNormalizationEngine('replaygain');
|
||||
}}
|
||||
>
|
||||
{t('settings.normalizationReplayGain')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${auth.normalizationEngine === 'loudness' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => {
|
||||
auth.setReplayGainEnabled(false);
|
||||
if (auth.normalizationEngine !== 'loudness') auth.setLoudnessTargetLufs(-12);
|
||||
auth.setNormalizationEngine('loudness');
|
||||
}}
|
||||
>
|
||||
{t('settings.normalizationLufs')}
|
||||
</button>
|
||||
</div>
|
||||
{auth.normalizationEngine === 'replaygain' && (
|
||||
<div style={{ paddingLeft: '1rem', marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 100 }}>
|
||||
{t('settings.replayGainPreGain')}
|
||||
</span>
|
||||
<input
|
||||
type="range" min={0} max={6} step={0.5}
|
||||
value={auth.replayGainPreGainDb}
|
||||
onChange={e => auth.setReplayGainPreGainDb(Number(e.target.value))}
|
||||
style={{ flex: 1, minWidth: 80, maxWidth: 160 }}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)', minWidth: 40, textAlign: 'right' }}>
|
||||
{auth.replayGainPreGainDb > 0 ? `+${auth.replayGainPreGainDb}` : auth.replayGainPreGainDb} dB
|
||||
</span>
|
||||
<div className="settings-norm-block">
|
||||
<div className="settings-norm-field">
|
||||
<div className="settings-norm-row">
|
||||
<span className="settings-norm-label">{t('settings.replayGainMode')}</span>
|
||||
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'auto' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '4px 14px' }}
|
||||
onClick={() => auth.setReplayGainMode('auto')}
|
||||
>
|
||||
{t('settings.replayGainAuto')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'track' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '4px 14px' }}
|
||||
onClick={() => auth.setReplayGainMode('track')}
|
||||
>
|
||||
{t('settings.replayGainTrack')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${auth.replayGainMode === 'album' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 12, padding: '4px 14px' }}
|
||||
onClick={() => auth.setReplayGainMode('album')}
|
||||
>
|
||||
{t('settings.replayGainAlbum')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{auth.replayGainMode === 'auto' && (
|
||||
<div className="settings-norm-help">{t('settings.replayGainAutoDesc')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 100 }}>
|
||||
{t('settings.replayGainFallback')}
|
||||
</span>
|
||||
<input
|
||||
type="range" min={-6} max={0} step={0.5}
|
||||
value={auth.replayGainFallbackDb}
|
||||
onChange={e => auth.setReplayGainFallbackDb(Number(e.target.value))}
|
||||
style={{ flex: 1, minWidth: 80, maxWidth: 160 }}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)', minWidth: 40, textAlign: 'right' }}>
|
||||
{auth.replayGainFallbackDb > 0 ? `+${auth.replayGainFallbackDb}` : auth.replayGainFallbackDb} dB
|
||||
</span>
|
||||
<div className="settings-norm-field">
|
||||
<div className="settings-norm-row">
|
||||
<span className="settings-norm-label">{t('settings.replayGainPreGain')}</span>
|
||||
<input
|
||||
type="range" min={0} max={6} step={0.5}
|
||||
value={auth.replayGainPreGainDb}
|
||||
onChange={e => auth.setReplayGainPreGainDb(Number(e.target.value))}
|
||||
/>
|
||||
<span className="settings-norm-value">
|
||||
{auth.replayGainPreGainDb > 0 ? `+${auth.replayGainPreGainDb}` : auth.replayGainPreGainDb} dB
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-norm-help">{t('settings.replayGainPreGainDesc')}</div>
|
||||
</div>
|
||||
<div className="settings-norm-field">
|
||||
<div className="settings-norm-row">
|
||||
<span className="settings-norm-label">{t('settings.replayGainFallback')}</span>
|
||||
<input
|
||||
type="range" min={-6} max={0} step={0.5}
|
||||
value={auth.replayGainFallbackDb}
|
||||
onChange={e => auth.setReplayGainFallbackDb(Number(e.target.value))}
|
||||
/>
|
||||
<span className="settings-norm-value">
|
||||
{auth.replayGainFallbackDb > 0 ? `+${auth.replayGainFallbackDb}` : auth.replayGainFallbackDb} dB
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-norm-help">{t('settings.replayGainFallbackDesc')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{auth.normalizationEngine === 'loudness' && (
|
||||
<div className="settings-norm-block">
|
||||
<div className="settings-norm-field">
|
||||
<div className="settings-norm-row">
|
||||
<span className="settings-norm-label">{t('settings.loudnessTargetLufs')}</span>
|
||||
<LoudnessLufsButtonGroup value={auth.loudnessTargetLufs} onSelect={auth.setLoudnessTargetLufs} />
|
||||
</div>
|
||||
<div className="settings-norm-help">{t('settings.loudnessTargetLufsDesc')}</div>
|
||||
</div>
|
||||
<div className="settings-norm-field">
|
||||
<div className="settings-norm-row">
|
||||
<span className="settings-norm-label">{t('settings.loudnessPreAnalysisAttenuation')}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={-24}
|
||||
max={0}
|
||||
step={0.5}
|
||||
value={auth.loudnessPreAnalysisAttenuationDb}
|
||||
onChange={e => auth.setLoudnessPreAnalysisAttenuationDb(Number(e.target.value))}
|
||||
/>
|
||||
<span className="settings-norm-value">
|
||||
{auth.loudnessPreAnalysisAttenuationDb} dB
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
style={{ flexShrink: 0 }}
|
||||
disabled={
|
||||
auth.loudnessPreAnalysisAttenuationDb === DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB
|
||||
}
|
||||
onClick={() => auth.resetLoudnessPreAnalysisAttenuationDbDefault()}
|
||||
data-tooltip={t('settings.loudnessPreAnalysisAttenuationReset')}
|
||||
aria-label={t('settings.loudnessPreAnalysisAttenuationReset')}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="settings-norm-help">{t('settings.loudnessPreAnalysisAttenuationDesc')}</div>
|
||||
</div>
|
||||
<div className="settings-norm-note">{t('settings.loudnessFirstPlayNote')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user