"use client"; import * as React from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Inbox, ChevronRight } from "lucide-react"; import { type Job } from "@/lib/api"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { EmptyState } from "@/components/ui/empty-state"; import { JobSheet } from "@/components/job-sheet"; import { useMotionOK, spring } from "@/lib/motion"; import { cn } from "@/lib/utils"; export type JobsSnapshot = { jobs: Job[]; counts: Record; pending: number; }; /** * `queued`, `searching` and `downloading` used to be the same amber pill and * differed only by the word inside, so the three stages of a request looked * identical at a glance. They are distinct stages with distinct waits, so they * now read as distinct. */ const STATE: Record = { queued: { tone: "neutral", label: "Queued" }, searching: { tone: "accent", label: "Searching", live: true }, downloading: { tone: "warn", label: "Downloading", live: true }, review: { tone: "accent", label: "Needs review" }, imported: { tone: "ok", label: "In library" }, failed: { tone: "err", label: "Failed" }, }; export function Requests({ snap, refresh }: { snap: JobsSnapshot | null; refresh: () => void }) { const ok = useMotionOK(); const [open, setOpen] = React.useState(null); if (!snap) return null; const { jobs, counts } = snap; const active = (counts.queued ?? 0) + (counts.searching ?? 0) + (counts.downloading ?? 0); const done = counts.imported ?? 0; const failed = counts.failed ?? 0; const held = counts.review ?? 0; const total = active + done; const stats: [string, number, string][] = [ ["In flight", active, "text-accent"], ["In library", done, "text-ok"], ["Needs review", held, held > 0 ? "text-warn" : "text-dim"], ["Failed", failed, failed > 0 ? "text-err" : "text-dim"], ]; // Kept in sync with the poll so the sheet updates live while it is open. const current = jobs.find((j) => j.id === open) ?? null; return ( <>
{stats.map(([label, n, colour]) => ( {n} {label} ))}
{active > 0 && total > 0 && ( )} {jobs.length === 0 ? ( ) : (
{jobs.map((j) => { const s = STATE[j.state] ?? STATE.queued; return ( setOpen(j.id)} aria-label={`${j.title} by ${j.artist} — ${s.label}. Show details`} className="card focus-ring flex items-center gap-3 rounded-card py-2.5 pl-2.5 pr-2 text-left transition-colors hover:bg-[var(--surface-2)]" >
{j.cover && ( /* eslint-disable-next-line @next/next/no-img-element */ (e.currentTarget.style.visibility = "hidden")} /> )}
{j.title}
{[j.artist, j.album].filter(Boolean).join(" · ")}
{j.detail && j.state === "failed" && (
{j.detail}
)}
{s.live && ( )} {s.label}
); })}
)} setOpen(null)} onChanged={refresh} /> ); }