feat(queue): collapse ReplayGain into a click-to-expand badge

Per @cucadmuh's feedback: the presence of ReplayGain matters more than
the actual numbers. The tech bar now shows a small 'RG' pill on line 1
whenever the track has any RG metadata; hovering reveals the values via
tooltip, clicking persistently expands the second line.

- Source icon stays inline before the format string (was getting
  separated from FLAC by the centred stack layout)
- Pill uses --accent-tinted color-mix so it adapts to every theme
- ChevronDown rotates on expand for a clear affordance
- New persisted state: themeStore.expandReplayGain (default collapsed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-19 12:34:52 +02:00
parent 6456f13bde
commit e07ef0ebf1
3 changed files with 86 additions and 24 deletions
+40 -21
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useMemo } from 'react';
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio, HardDrive } from 'lucide-react';
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio, HardDrive, ChevronDown } from 'lucide-react';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore';
import { useCachedUrl } from './CachedImage';
@@ -8,6 +8,7 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
import { useLyricsStore } from '../store/lyricsStore';
import { useDragDrop } from '../contexts/DragDropContext';
import LyricsPane from './LyricsPane';
@@ -267,6 +268,8 @@ export default function QueuePanel() {
const [showRemainingTime, setShowRemainingTime] = useState(false);
const [showCrossfadePopover, setShowCrossfadePopover] = useState(false);
const expandReplayGain = useThemeStore(s => s.expandReplayGain);
const setExpandReplayGain = useThemeStore(s => s.setExpandReplayGain);
const crossfadeBtnRef = useRef<HTMLButtonElement>(null);
const crossfadePopoverRef = useRef<HTMLDivElement>(null);
@@ -454,28 +457,44 @@ export default function QueuePanel() {
const baseLine = baseParts.join(' · ');
const rgLine = rgParts.join(' · ');
if (!baseLine && !rgLine && !playbackSource) return null;
const showRgLine = expandReplayGain && !!rgLine;
return (
<div className={`queue-current-tech${rgLine ? ' queue-current-tech--two-line' : ''}`}>
{playbackSource && (
<span
className="queue-current-tech-source"
data-tooltip={
playbackSource === 'offline'
? t('queue.sourceOffline')
: playbackSource === 'hot'
? t('queue.sourceHot')
: t('queue.sourceStream')
}
aria-hidden
>
{playbackSource === 'offline' && <FolderOpen size={11} strokeWidth={2.25} />}
{playbackSource === 'hot' && <HardDrive size={11} strokeWidth={2.25} />}
{playbackSource === 'stream' && <Waves size={11} strokeWidth={2.25} />}
</span>
)}
<div className={`queue-current-tech${showRgLine ? ' queue-current-tech--two-line' : ''}`}>
<div className="queue-current-tech-stack">
{baseLine && <span className="queue-current-tech-main">{baseLine}</span>}
{rgLine && (
<div className="queue-current-tech-row">
{playbackSource && (
<span
className="queue-current-tech-source"
data-tooltip={
playbackSource === 'offline'
? t('queue.sourceOffline')
: playbackSource === 'hot'
? t('queue.sourceHot')
: t('queue.sourceStream')
}
aria-hidden
>
{playbackSource === 'offline' && <FolderOpen size={11} strokeWidth={2.25} />}
{playbackSource === 'hot' && <HardDrive size={11} strokeWidth={2.25} />}
{playbackSource === 'stream' && <Waves size={11} strokeWidth={2.25} />}
</span>
)}
{baseLine && <span className="queue-current-tech-main">{baseLine}</span>}
{rgLine && (
<button
type="button"
className={`queue-current-tech-rg-badge${showRgLine ? ' queue-current-tech-rg-badge--open' : ''}`}
data-tooltip={`${t('queue.replayGain')} · ${rgLine}`}
aria-expanded={showRgLine}
aria-label={t('queue.replayGain')}
onClick={() => setExpandReplayGain(!expandReplayGain)}
>
RG
<ChevronDown size={9} strokeWidth={2.5} />
</button>
)}
</div>
{showRgLine && (
<span className="queue-current-tech-rg">
<span className="queue-current-tech-rg-label">{t('queue.replayGain')}</span>
{' · '}{rgLine}
+4
View File
@@ -24,6 +24,8 @@ interface ThemeState {
setShowBitrate: (v: boolean) => void;
showRemainingTime: boolean;
setShowRemainingTime: (v: boolean) => void;
expandReplayGain: boolean;
setExpandReplayGain: (v: boolean) => void;
}
export function getScheduledTheme(state: Pick<ThemeState, 'enableThemeScheduler' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'>): string {
@@ -63,6 +65,8 @@ export const useThemeStore = create<ThemeState>()(
setShowBitrate: (v) => set({ showBitrate: v }),
showRemainingTime: false,
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
expandReplayGain: false,
setExpandReplayGain: (v) => set({ expandReplayGain: v }),
}),
{
name: 'psysonic_theme',
+42 -3
View File
@@ -1725,9 +1725,10 @@
text-overflow: ellipsis;
}
/* Two-line layout: base info on top, ReplayGain underneath. The stack
wraps both spans so the source icon stays vertically centered next to
the whole text block. */
/* Two-line layout: base info + RG badge on top, optional ReplayGain
values underneath when the badge is expanded. The stack wraps both
rows so the source icon stays vertically centered next to the whole
text block. */
.queue-current-tech-stack {
min-width: 0;
display: flex;
@@ -1736,6 +1737,44 @@
gap: 1px;
}
.queue-current-tech-row {
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
/* Pill-shaped toggle that signals "this track has ReplayGain metadata"
without committing screen space to the numbers — values appear in the
tooltip and on the second line when expanded. */
.queue-current-tech-rg-badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 2px;
padding: 1px 5px 1px 6px;
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
color: var(--accent);
border-radius: 999px;
font: inherit;
font-size: 8px;
letter-spacing: 0.06em;
cursor: pointer;
transition: background 0.12s, border-color 0.12s, transform 0.12s;
}
.queue-current-tech-rg-badge:hover {
background: color-mix(in srgb, var(--accent) 22%, transparent);
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.queue-current-tech-rg-badge svg {
transition: transform 0.18s ease;
}
.queue-current-tech-rg-badge--open svg {
transform: rotate(180deg);
}
.queue-current-tech-rg {
min-width: 0;
max-width: 100%;