mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(queue-panel): H4 — split QueuePanel.tsx 1256 → 383 LOC across 11 files (#667)
* refactor(queue-panel): H4 — extract helpers + Save/LoadPlaylistModal Pure code-move: formatTime, formatQueueReplayGainParts, renderStars and the DurationMode type → utils/queuePanelHelpers.tsx; the two playlist modals → own files under components/queuePanel/. QueuePanel.tsx: 1256 → 1104 LOC. * refactor(queue-panel): H4 — extract QueueHeader Pure code-move: the title/count/duration/collapse-button header → its own component file. No prop or behaviour changes. QueuePanel.tsx: 1104 → 1004 LOC. * refactor(queue-panel): H4 — extract QueueCurrentTrack + QueueLufsTargetMenu The currently-playing track block (cover, info, replay-gain / LUFS badge with its target-listbox portal) moves into two own files. Pure code-move via prop plumbing. setLoudnessTargetLufs is typed as LoudnessLufsPreset throughout the new components. QueuePanel.tsx: 1004 → 812 LOC. * refactor(queue-panel): H4 — extract useQueuePanelDrag hook Moves the psy-drag wiring (hit-test registration, drop-inside dispatch for song/songs/album/queue_reorder payloads, drop-outside removal) into its own hook. Drops the dead isRadioDrag variable since the parsedData.type === 'radio' guard inside onPsyDrop already handles that case. QueuePanel.tsx: 812 → 715 LOC. * refactor(queue-panel): H4 — extract useQueueLufsTgtPopover hook Pulls the LUFS-target popover open-state, button/menu refs, fixed-position recompute on open/resize/scroll, and auto-close-when-RG-collapses out of QueuePanel. QueuePanel.tsx: 715 → 662 LOC. * refactor(queue-panel): H4 — extract QueueToolbar The toolbar-button switch (shuffle/save/load/share/clear/gapless/crossfade/ infinite) and the crossfade popover (with its close-on-outside-click effect) move into one component. crossfadeBtnRef / crossfadePopoverRef and showCrossfadePopover state are now component-local — QueuePanel no longer sees them. QueuePanel.tsx: 662 → 541 LOC. * refactor(queue-panel): H4 — extract QueueList The OverlayScrollArea + queue.map block (with track rows, lucky-mix dice overlay, and radio/auto-added section dividers) moves into its own component. PlayerState['contextMenu'] + PlayerState['playTrack'] are re-used for prop typing, the local StartDrag alias matches the DragDropContext signature. QueuePanel.tsx: 541 → 434 LOC. * refactor(queue-panel): H4 — extract QueueTabBar + useQueueAutoScroll, final cleanup QueueTabBar is the bottom queue/lyrics/info tab switcher. useQueueAutoScroll groups the three list-scroll effects (publish scrollTop reader, restore pending snapshot, scroll next track into view on advance). Drops the dead toggleQueue and replayGainMode selectors plus the now-unused Play, Radio, MicVocal, ListMusic, Info imports and OverlayScrollArea. QueuePanel.tsx: 434 → 383 LOC. Every new file under 400.
This commit is contained in:
committed by
GitHub
parent
34cc311b4d
commit
8ff630cb5c
@@ -0,0 +1,111 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ChevronDown, ListMusic } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import type { DurationMode } from '../../utils/queuePanelHelpers';
|
||||
|
||||
interface Props {
|
||||
queue: Track[];
|
||||
queueIndex: number;
|
||||
activePlaylist: { id: string; name: string } | null;
|
||||
isNowPlayingCollapsed: boolean;
|
||||
setIsNowPlayingCollapsed: (v: boolean) => void;
|
||||
durationMode: DurationMode;
|
||||
setDurationMode: (m: DurationMode) => void;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export function QueueHeader({
|
||||
queue, queueIndex, activePlaylist, isNowPlayingCollapsed,
|
||||
setIsNowPlayingCollapsed, durationMode, setDurationMode, t,
|
||||
}: Props) {
|
||||
const currentTime = usePlayerStore((s) => Math.floor(s.currentTime / 30) * 30);
|
||||
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
||||
|
||||
const totalSecs = useMemo(() =>
|
||||
queue.reduce((acc: number, track: Track) => acc + (track.duration || 0), 0),
|
||||
[queue]
|
||||
);
|
||||
const futureTracksDuration = useMemo(() =>
|
||||
queue.slice(queueIndex + 1).reduce((acc: number, track: Track) => acc + (track.duration || 0), 0),
|
||||
[queue, queueIndex]
|
||||
);
|
||||
|
||||
const remainingSecs = Math.max(0, (queue[queueIndex]?.duration ?? 0) - currentTime + futureTracksDuration);
|
||||
|
||||
const fmt = (secs: number) => {
|
||||
const h = Math.floor(secs / 3600);
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
const s = secs % 60;
|
||||
return h > 0 ? `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` : `${m}:${s.toString().padStart(2, "0")}`;
|
||||
};
|
||||
const fmtEta = (secs: number) => {
|
||||
const finishTime = new Date(Date.now() + secs * 1000);
|
||||
return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(finishTime);
|
||||
};
|
||||
|
||||
let dur: string | null = null;
|
||||
if (queue.length > 0) {
|
||||
if (durationMode === 'total') dur = fmt(Math.floor(totalSecs));
|
||||
else if (durationMode === 'remaining') dur = `-${fmt(Math.floor(remainingSecs))}`;
|
||||
else dur = fmtEta(remainingSecs);
|
||||
}
|
||||
|
||||
const nextMode: DurationMode =
|
||||
durationMode === 'total' ? 'remaining' :
|
||||
durationMode === 'remaining' ? 'eta' : 'total';
|
||||
const nextTooltipKey =
|
||||
nextMode === 'total' ? 'queue.showTotal' :
|
||||
nextMode === 'remaining' ? 'queue.showRemaining' : 'queue.showEta';
|
||||
|
||||
const isEta = durationMode === 'eta';
|
||||
|
||||
return (
|
||||
<div className="queue-header">
|
||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: "8px", minWidth: 0 }}>
|
||||
<h2 style={{ fontSize: "16px", fontWeight: 700, margin: 0, flexShrink: 0 }}>{t("queue.title")}</h2>
|
||||
{queue.length > 0 && (
|
||||
<span style={{ fontSize: "13px", color: "var(--text-muted)", whiteSpace: "nowrap", userSelect: "none" }}>
|
||||
({queueIndex + 1}/{queue.length})
|
||||
</span>
|
||||
)}
|
||||
{dur !== null && (
|
||||
<span
|
||||
onClick={() => setDurationMode(nextMode)}
|
||||
data-tooltip={t(nextTooltipKey)}
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: isEta ? (isPlaying ? "var(--accent)" : "var(--text-muted)") : "var(--accent)",
|
||||
opacity: isEta && !isPlaying ? 0.5 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
· {dur}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{activePlaylist && (
|
||||
<div className="truncate" style={{ fontSize: "11px", color: "var(--text-muted)", marginTop: "2px", display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<ListMusic size={10} style={{ flexShrink: 0 }} />
|
||||
<span className="truncate">{activePlaylist.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="queue-action-btn"
|
||||
onClick={() => queue.length > 0 && setIsNowPlayingCollapsed(!isNowPlayingCollapsed)}
|
||||
disabled={queue.length === 0}
|
||||
data-tooltip={queue.length === 0 ? t('queue.emptyQueue') : (isNowPlayingCollapsed ? t('queue.showNowPlaying') : t('queue.hideNowPlaying'))}
|
||||
aria-label={queue.length === 0 ? t('queue.emptyQueue') : (isNowPlayingCollapsed ? t('queue.showNowPlaying') : t('queue.hideNowPlaying'))}
|
||||
aria-expanded={!isNowPlayingCollapsed}
|
||||
style={{ marginLeft: '8px', opacity: queue.length === 0 ? 0.3 : 1, cursor: queue.length === 0 ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<ChevronDown size={18} style={{ transform: isNowPlayingCollapsed ? 'rotate(-90deg)' : 'none', transition: 'transform 0.2s ease' }} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user