mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
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:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useRef, useMemo } from 'react';
|
import React, { useState, useRef, useMemo } from 'react';
|
||||||
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
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 { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
|
||||||
import { usePlaylistStore } from '../store/playlistStore';
|
import { usePlaylistStore } from '../store/playlistStore';
|
||||||
import { useCachedUrl } from './CachedImage';
|
import { useCachedUrl } from './CachedImage';
|
||||||
@@ -8,6 +8,7 @@ import { useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { useThemeStore } from '../store/themeStore';
|
||||||
import { useLyricsStore } from '../store/lyricsStore';
|
import { useLyricsStore } from '../store/lyricsStore';
|
||||||
import { useDragDrop } from '../contexts/DragDropContext';
|
import { useDragDrop } from '../contexts/DragDropContext';
|
||||||
import LyricsPane from './LyricsPane';
|
import LyricsPane from './LyricsPane';
|
||||||
@@ -267,6 +268,8 @@ export default function QueuePanel() {
|
|||||||
|
|
||||||
const [showRemainingTime, setShowRemainingTime] = useState(false);
|
const [showRemainingTime, setShowRemainingTime] = useState(false);
|
||||||
const [showCrossfadePopover, setShowCrossfadePopover] = 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 crossfadeBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
const crossfadePopoverRef = useRef<HTMLDivElement>(null);
|
const crossfadePopoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -454,28 +457,44 @@ export default function QueuePanel() {
|
|||||||
const baseLine = baseParts.join(' · ');
|
const baseLine = baseParts.join(' · ');
|
||||||
const rgLine = rgParts.join(' · ');
|
const rgLine = rgParts.join(' · ');
|
||||||
if (!baseLine && !rgLine && !playbackSource) return null;
|
if (!baseLine && !rgLine && !playbackSource) return null;
|
||||||
|
const showRgLine = expandReplayGain && !!rgLine;
|
||||||
return (
|
return (
|
||||||
<div className={`queue-current-tech${rgLine ? ' queue-current-tech--two-line' : ''}`}>
|
<div className={`queue-current-tech${showRgLine ? ' 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-stack">
|
<div className="queue-current-tech-stack">
|
||||||
{baseLine && <span className="queue-current-tech-main">{baseLine}</span>}
|
<div className="queue-current-tech-row">
|
||||||
{rgLine && (
|
{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">
|
||||||
<span className="queue-current-tech-rg-label">{t('queue.replayGain')}</span>
|
<span className="queue-current-tech-rg-label">{t('queue.replayGain')}</span>
|
||||||
{' · '}{rgLine}
|
{' · '}{rgLine}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface ThemeState {
|
|||||||
setShowBitrate: (v: boolean) => void;
|
setShowBitrate: (v: boolean) => void;
|
||||||
showRemainingTime: boolean;
|
showRemainingTime: boolean;
|
||||||
setShowRemainingTime: (v: boolean) => void;
|
setShowRemainingTime: (v: boolean) => void;
|
||||||
|
expandReplayGain: boolean;
|
||||||
|
setExpandReplayGain: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScheduledTheme(state: Pick<ThemeState, 'enableThemeScheduler' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'>): string {
|
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 }),
|
setShowBitrate: (v) => set({ showBitrate: v }),
|
||||||
showRemainingTime: false,
|
showRemainingTime: false,
|
||||||
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
||||||
|
expandReplayGain: false,
|
||||||
|
setExpandReplayGain: (v) => set({ expandReplayGain: v }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'psysonic_theme',
|
name: 'psysonic_theme',
|
||||||
|
|||||||
+42
-3
@@ -1725,9 +1725,10 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two-line layout: base info on top, ReplayGain underneath. The stack
|
/* Two-line layout: base info + RG badge on top, optional ReplayGain
|
||||||
wraps both spans so the source icon stays vertically centered next to
|
values underneath when the badge is expanded. The stack wraps both
|
||||||
the whole text block. */
|
rows so the source icon stays vertically centered next to the whole
|
||||||
|
text block. */
|
||||||
.queue-current-tech-stack {
|
.queue-current-tech-stack {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1736,6 +1737,44 @@
|
|||||||
gap: 1px;
|
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 {
|
.queue-current-tech-rg {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user