// OVE — playlist queue bar (v6.7). // // Slim strip directly under the header, visible while a playlist queue is // active. Shows what's playing, where we are in the list, lazy-load status, // prev/next/jump/stop controls and the expandable track list. The queue // itself (lib/ava/queue.ts) fetches ONE track at a time — this bar surfaces // that promise to the visitor. 'use client' import { useState } from 'react' import { AnimatePresence, motion } from 'framer-motion' import { AlertTriangle, ChevronDown, ChevronUp, Clock, ListMusic, Loader2, PauseCircle, Play, RotateCcw, Settings2, SkipBack, SkipForward, X, } from 'lucide-react' import { Button } from '@/components/ui/button' import { queueJump, queueNext, queuePrev, replayQueue, stopQueue, useQueueStore, type QueueStatus, } from '@/lib/ava/queue' import { useAvaStore } from '@/lib/store' import { thinScrollbar } from '@/components/ava/shared' import { cn } from '@/lib/utils' function fmtDuration(sec: number | null): string { if (sec === null || !Number.isFinite(sec) || sec <= 0) return '' const m = Math.floor(sec / 60) const s = Math.floor(sec % 60) return `${m}:${String(s).padStart(2, '0')}` } function statusChip( status: QueueStatus, preloading: boolean, updating: boolean, ): { label: string; cls: string } | null { switch (status) { case 'loading': // v6.8: background yt-dlp self-update outranks the plain "fetching" return updating ? { label: 'updating engine…', cls: 'border-amber-400/40 bg-amber-400/10 text-amber-300' } : { label: 'fetching', cls: 'border-zinc-700 bg-zinc-800/60 text-zinc-300' } case 'playing': // v6.8: a look-ahead resolve stuck in the self-update loop outranks // the plain "next up ready" chip return updating ? { label: 'updating engine…', cls: 'border-amber-400/40 bg-amber-400/10 text-amber-300' } : preloading ? { label: 'next up ready', cls: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300' } : null case 'done': return { label: 'finished', cls: 'border-zinc-700 bg-zinc-800/60 text-zinc-300' } case 'blocked': return { label: 'paused', cls: 'border-amber-400/40 bg-amber-400/10 text-amber-300' } case 'error': return { label: 'stopped', cls: 'border-rose-400/40 bg-rose-400/10 text-rose-300' } default: return null } } /** Tiny icon button used for prev / next / expand / close. */ function IconButton({ onClick, disabled, title, children, }: { onClick: () => void disabled?: boolean title: string children: React.ReactNode }) { return ( ) } export function QueueBar() { const status = useQueueStore((s) => s.status) const title = useQueueStore((s) => s.title) const entries = useQueueStore((s) => s.entries) const index = useQueueStore((s) => s.index) const trackTitle = useQueueStore((s) => s.trackTitle) const lastError = useQueueStore((s) => s.lastError) const lastErrorCode = useQueueStore((s) => s.lastErrorCode) const preloading = useQueueStore((s) => s.preloading) const updating = useQueueStore((s) => s.updating) const setTab = useAvaStore((s) => s.setTab) const [expanded, setExpanded] = useState(false) if (status === 'idle' || entries.length === 0) return null const chip = statusChip(status, preloading, updating) const current = index >= 0 && index < entries.length ? entries[index] : null return (
{/* playlist identity + position */}
{title || 'Playlist'} {index + 1}/{entries.length} {chip && ( {status === 'loading' && } {chip.label} )}
{/* now playing / state message */}
{status === 'done' ? (

Queue finished — {entries.length} track{entries.length === 1 ? '' : 's'} played.

) : status === 'blocked' ? (

The browser blocked auto-advance — press Resume to keep the queue going.

) : status === 'error' ? (

{lastError ?? 'The queue stopped after repeated failures.'}

) : status === 'loading' && updating ? (

Updating in Background — the audio engine on the server is updating itself; your music resumes in a moment.

) : (

{status === 'loading' && } {status === 'playing' && preloading && } {current?.title ?? trackTitle} {status === 'playing' && preloading && ( · next up is ready )}

)}
{/* controls */}
{status === 'done' ? ( ) : status === 'blocked' ? ( ) : status === 'error' ? ( <> {lastErrorCode === 'bot-wall' && ( )} ) : ( <> = entries.length - 1} title="Next track" > )} setExpanded((v) => !v)} title={expanded ? 'Hide the track list' : 'Show the track list'} > {expanded ? : }
{/* expandable track list */} {expanded && (

QUEUE — CLICK A TRACK TO JUMP

    {entries.map((e, i) => { const isCurrent = i === index && status !== 'done' const dur = fmtDuration(e.durationSec) return (
  • ) })}
)}

Lazy queue — each track is fetched from the source only right before it plays, one at a time, so playlist hosts are never hit with bulk requests.

) }