diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 720120db..5cdabf8b 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -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(null); const crossfadePopoverRef = useRef(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 ( -
- {playbackSource && ( - - {playbackSource === 'offline' && } - {playbackSource === 'hot' && } - {playbackSource === 'stream' && } - - )} +
- {baseLine && {baseLine}} - {rgLine && ( +
+ {playbackSource && ( + + {playbackSource === 'offline' && } + {playbackSource === 'hot' && } + {playbackSource === 'stream' && } + + )} + {baseLine && {baseLine}} + {rgLine && ( + + )} +
+ {showRgLine && ( {t('queue.replayGain')} {' · '}{rgLine} diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts index d857757c..2bcb427a 100644 --- a/src/store/themeStore.ts +++ b/src/store/themeStore.ts @@ -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): string { @@ -63,6 +65,8 @@ export const useThemeStore = create()( setShowBitrate: (v) => set({ showBitrate: v }), showRemainingTime: false, setShowRemainingTime: (v) => set({ showRemainingTime: v }), + expandReplayGain: false, + setExpandReplayGain: (v) => set({ expandReplayGain: v }), }), { name: 'psysonic_theme', diff --git a/src/styles/layout.css b/src/styles/layout.css index ce43f3a0..859a47a3 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -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%;