mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(settings): make normalization modes exclusive and refine LUFS UI
Replace the mixed ReplayGain/Loudness controls with a single exclusive mode selector, hide RG-specific controls when LUFS is active, and align the queue tech badge behavior between RG and LUFS. Also update LUFS targets to -10..-16 ordering and default loudness target to -12.
This commit is contained in:
@@ -483,16 +483,16 @@ export default function QueuePanel() {
|
|||||||
const rgParts = formatQueueReplayGainParts(currentTrack, t);
|
const rgParts = formatQueueReplayGainParts(currentTrack, t);
|
||||||
const baseLine = baseParts.join(' · ');
|
const baseLine = baseParts.join(' · ');
|
||||||
const rgLine = rgParts.join(' · ');
|
const rgLine = rgParts.join(' · ');
|
||||||
|
const isLoudnessActive = normalizationEngine === 'loudness' || normalizationEngineLive === 'loudness';
|
||||||
const liveGainLabel = normalizationNowDb != null
|
const liveGainLabel = normalizationNowDb != null
|
||||||
? `${normalizationNowDb >= 0 ? '+' : ''}${normalizationNowDb.toFixed(2)} dB`
|
? `${normalizationNowDb >= 0 ? '+' : ''}${normalizationNowDb.toFixed(2)} dB`
|
||||||
: '—';
|
: '—';
|
||||||
const targetLabel = normalizationEngineLive === 'loudness'
|
const targetLabel = normalizationTargetLufs != null
|
||||||
? (normalizationTargetLufs != null ? `${normalizationTargetLufs} LUFS` : '-16 LUFS')
|
? `${normalizationTargetLufs} LUFS`
|
||||||
: normalizationEngineLive === 'replaygain'
|
: '-16 LUFS';
|
||||||
? `ReplayGain (${replayGainMode})`
|
|
||||||
: t('queue.normOff', { defaultValue: 'Off' });
|
|
||||||
if (!baseLine && !rgLine && !playbackSource) return null;
|
if (!baseLine && !rgLine && !playbackSource) return null;
|
||||||
const showRgLine = expandReplayGain && !!rgLine;
|
const showRgLine = !isLoudnessActive && expandReplayGain && !!rgLine;
|
||||||
|
const showLufsLine = isLoudnessActive && expandReplayGain;
|
||||||
return (
|
return (
|
||||||
<div className={`queue-current-tech${showRgLine ? ' queue-current-tech--two-line' : ''}`}>
|
<div className={`queue-current-tech${showRgLine ? ' queue-current-tech--two-line' : ''}`}>
|
||||||
<div className="queue-current-tech-stack">
|
<div className="queue-current-tech-stack">
|
||||||
@@ -515,7 +515,7 @@ export default function QueuePanel() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{baseLine && <span className="queue-current-tech-main">{baseLine}</span>}
|
{baseLine && <span className="queue-current-tech-main">{baseLine}</span>}
|
||||||
{rgLine && (
|
{!isLoudnessActive && rgLine && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`queue-current-tech-rg-badge${showRgLine ? ' queue-current-tech-rg-badge--open' : ''}`}
|
className={`queue-current-tech-rg-badge${showRgLine ? ' queue-current-tech-rg-badge--open' : ''}`}
|
||||||
@@ -528,6 +528,19 @@ export default function QueuePanel() {
|
|||||||
<ChevronDown size={9} strokeWidth={2.5} />
|
<ChevronDown size={9} strokeWidth={2.5} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{isLoudnessActive && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`queue-current-tech-rg-badge${showLufsLine ? ' queue-current-tech-rg-badge--open' : ''}`}
|
||||||
|
data-tooltip={`LUFS · ${liveGainLabel} · TGT · ${targetLabel}`}
|
||||||
|
aria-expanded={showLufsLine}
|
||||||
|
aria-label="LUFS"
|
||||||
|
onClick={() => setExpandReplayGain(!expandReplayGain)}
|
||||||
|
>
|
||||||
|
LUFS
|
||||||
|
<ChevronDown size={9} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showRgLine && (
|
{showRgLine && (
|
||||||
<span className="queue-current-tech-rg">
|
<span className="queue-current-tech-rg">
|
||||||
@@ -535,12 +548,12 @@ export default function QueuePanel() {
|
|||||||
{' · '}{rgLine}
|
{' · '}{rgLine}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(normalizationEngine === 'loudness' || normalizationEngineLive === 'loudness') && (
|
{showLufsLine && (
|
||||||
<span className="queue-current-tech-rg">
|
<span className="queue-current-tech-rg">
|
||||||
<span className="queue-current-tech-rg-label">{t('queue.normNow', { defaultValue: 'Now' })}</span>
|
<span className="queue-current-tech-rg-label">Loudness</span>
|
||||||
{' · '}{liveGainLabel}
|
{' · '}{liveGainLabel}
|
||||||
{' · '}
|
{' · '}
|
||||||
<span className="queue-current-tech-rg-label">{t('queue.normTarget', { defaultValue: 'Target' })}</span>
|
<span className="queue-current-tech-rg-label">TGT</span>
|
||||||
{' · '}{targetLabel}
|
{' · '}{targetLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+55
-47
@@ -2217,59 +2217,67 @@ export default function Settings() {
|
|||||||
icon={<Music2 size={16} />}
|
icon={<Music2 size={16} />}
|
||||||
>
|
>
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
{/* Replay Gain */}
|
{/* Normalization */}
|
||||||
<div className="settings-toggle-row">
|
<div className="settings-toggle-row">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500 }}>{t('settings.replayGain')}</div>
|
<div style={{ fontWeight: 500 }}>{t('settings.normalization', { defaultValue: 'Normalization' })}</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.replayGainDesc')}</div>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
</div>
|
{t('settings.normalizationDesc', { defaultValue: 'Choose one normalization mode: ReplayGain or Loudness (LUFS).' })}
|
||||||
<label className="toggle-switch" aria-label={t('settings.replayGain')}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={auth.replayGainEnabled}
|
|
||||||
onChange={e => {
|
|
||||||
const on = e.target.checked;
|
|
||||||
auth.setReplayGainEnabled(on);
|
|
||||||
if (!on) auth.setNormalizationEngine('off');
|
|
||||||
else if (auth.normalizationEngine === 'off') auth.setNormalizationEngine('replaygain');
|
|
||||||
}}
|
|
||||||
id="replay-gain-toggle"
|
|
||||||
/>
|
|
||||||
<span className="toggle-track" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{auth.replayGainEnabled && (
|
|
||||||
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Engine:</span>
|
|
||||||
<button
|
|
||||||
className={`btn ${auth.normalizationEngine === 'replaygain' ? 'btn-primary' : 'btn-ghost'}`}
|
|
||||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
|
||||||
onClick={() => auth.setNormalizationEngine('replaygain')}
|
|
||||||
>
|
|
||||||
ReplayGain
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${auth.normalizationEngine === 'loudness' ? 'btn-primary' : 'btn-ghost'}`}
|
|
||||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
|
||||||
onClick={() => auth.setNormalizationEngine('loudness')}
|
|
||||||
>
|
|
||||||
Loudness
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
{auth.normalizationEngine !== 'off' && (
|
||||||
|
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
||||||
{auth.normalizationEngine === 'loudness' && (
|
{auth.normalizationEngine === 'loudness' && (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
|
||||||
Apple-like automatic normalization. ReplayGain tags are ignored in this mode.
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Target LUFS:</span>
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Target LUFS:</span>
|
||||||
<button
|
<button
|
||||||
className={`btn ${auth.loudnessTargetLufs === -16 ? 'btn-primary' : 'btn-ghost'}`}
|
className={`btn ${auth.loudnessTargetLufs === -10 ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||||
onClick={() => auth.setLoudnessTargetLufs(-16)}
|
onClick={() => auth.setLoudnessTargetLufs(-10)}
|
||||||
>
|
>
|
||||||
-16
|
-10
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn ${auth.loudnessTargetLufs === -12 ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
|
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||||
|
onClick={() => auth.setLoudnessTargetLufs(-12)}
|
||||||
|
>
|
||||||
|
-12
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn ${auth.loudnessTargetLufs === -14 ? 'btn-primary' : 'btn-ghost'}`}
|
className={`btn ${auth.loudnessTargetLufs === -14 ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
@@ -2279,11 +2287,11 @@ export default function Settings() {
|
|||||||
-14
|
-14
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn ${auth.loudnessTargetLufs === -12 ? 'btn-primary' : 'btn-ghost'}`}
|
className={`btn ${auth.loudnessTargetLufs === -16 ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
style={{ fontSize: 12, padding: '3px 12px' }}
|
||||||
onClick={() => auth.setLoudnessTargetLufs(-12)}
|
onClick={() => auth.setLoudnessTargetLufs(-16)}
|
||||||
>
|
>
|
||||||
-12
|
-16
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -2321,7 +2329,7 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{auth.replayGainEnabled && auth.normalizationEngine === 'replaygain' && (
|
{auth.normalizationEngine === 'replaygain' && (
|
||||||
<div style={{ paddingLeft: '1rem', marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
<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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 100 }}>
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 100 }}>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ interface AuthState {
|
|||||||
customGenreBlacklist: string[];
|
customGenreBlacklist: string[];
|
||||||
replayGainEnabled: boolean;
|
replayGainEnabled: boolean;
|
||||||
normalizationEngine: NormalizationEngine;
|
normalizationEngine: NormalizationEngine;
|
||||||
loudnessTargetLufs: -16 | -14 | -12;
|
loudnessTargetLufs: -16 | -14 | -12 | -10;
|
||||||
replayGainMode: 'track' | 'album' | 'auto';
|
replayGainMode: 'track' | 'album' | 'auto';
|
||||||
replayGainPreGainDb: number; // added to RG gain for tagged files (0…+6 dB)
|
replayGainPreGainDb: number; // added to RG gain for tagged files (0…+6 dB)
|
||||||
replayGainFallbackDb: number; // gain for untagged files / radio (-6…0 dB)
|
replayGainFallbackDb: number; // gain for untagged files / radio (-6…0 dB)
|
||||||
@@ -209,7 +209,7 @@ interface AuthState {
|
|||||||
setCustomGenreBlacklist: (v: string[]) => void;
|
setCustomGenreBlacklist: (v: string[]) => void;
|
||||||
setReplayGainEnabled: (v: boolean) => void;
|
setReplayGainEnabled: (v: boolean) => void;
|
||||||
setNormalizationEngine: (v: NormalizationEngine) => void;
|
setNormalizationEngine: (v: NormalizationEngine) => void;
|
||||||
setLoudnessTargetLufs: (v: -16 | -14 | -12) => void;
|
setLoudnessTargetLufs: (v: -16 | -14 | -12 | -10) => void;
|
||||||
setReplayGainMode: (v: 'track' | 'album' | 'auto') => void;
|
setReplayGainMode: (v: 'track' | 'album' | 'auto') => void;
|
||||||
setReplayGainPreGainDb: (v: number) => void;
|
setReplayGainPreGainDb: (v: number) => void;
|
||||||
setReplayGainFallbackDb: (v: number) => void;
|
setReplayGainFallbackDb: (v: number) => void;
|
||||||
@@ -321,7 +321,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
customGenreBlacklist: [],
|
customGenreBlacklist: [],
|
||||||
replayGainEnabled: false,
|
replayGainEnabled: false,
|
||||||
normalizationEngine: 'off',
|
normalizationEngine: 'off',
|
||||||
loudnessTargetLufs: -14,
|
loudnessTargetLufs: -12,
|
||||||
replayGainMode: 'auto',
|
replayGainMode: 'auto',
|
||||||
replayGainPreGainDb: 0,
|
replayGainPreGainDb: 0,
|
||||||
replayGainFallbackDb: 0,
|
replayGainFallbackDb: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user