import { Slider, Badge, Menu, MenuTrigger, MenuContent, MenuItem, IconButton, } from '@olly/modern-sk'; import { useTranslation } from 'react-i18next'; import { DndContext, closestCenter, PointerSensor, KeyboardSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, useSortable, verticalListSortingStrategy, sortableKeyboardCoordinates, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Icon } from '../common/Icon'; import { ArtTile } from '../common/ArtTile'; import { Marquee } from '../common/Marquee'; import { PlayingIndicator } from '../common/PlayingIndicator'; import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch'; import { goToIndex, removeFromQueue, moveInQueue, clearQueue, toggleShuffle, toggleLoop, type QueueEntry, } from '../../store/slices/queue'; import { toggleQueue } from '../../store/slices/player'; import { openTrackInfo } from '../../store/slices/ui'; import { useResolvedQueueEntry } from '../../hooks/useResolvedQueueEntry'; import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming'; export function QueuePanel() { const { t } = useTranslation(); const dispatch = useAppDispatch(); const queue = useAppSelector((s) => s.queue); const isPlaying = useAppSelector((s) => s.player.isPlaying); const isOpen = useAppSelector((s) => s.player.isQueueOpen); const hasEntries = queue.entries.length > 0; const isRadio = queue.source === 'radio'; const sourceLabel = queue.sourceName ?? queue.source; const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; dispatch(moveInQueue({ from: Number(active.id), to: Number(over.id) })); }; return ( ); } /** A queue row, resolving its display fields against the live Track cache so * enrichment updates show. The currently-playing entry is outlined and shows * a playing-bars indicator in place of the drag grip. */ function QueueRow({ id, entry, isCurrent, isPlaying, onPlay, onMoveNext, onRemove, }: { id: string; entry: QueueEntry; isCurrent: boolean; isPlaying: boolean; onPlay: () => void; onMoveNext: () => void; onRemove: () => void; }) { const { t } = useTranslation(); const dispatch = useAppDispatch(); const token = useAppSelector((s) => s.auth.accessToken); const resolved = useResolvedQueueEntry(entry); const albumTitle = resolved?.albumTitle ?? entry.albumTitle; const artUrl = getCoverUrl(resolved?.albumArtUrl) ?? (token && resolved?.hasCover ? getTrackCoverUrl(resolved.trackId, token, true) : undefined); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (