"use client"; import * as React from "react"; import { motion } from "framer-motion"; import { ArrowLeft, Disc3, Music2 } from "lucide-react"; import { api, type Me, type Track } from "@/lib/api"; import { tint } from "@/lib/colour"; import { cn } from "@/lib/utils"; import { Aurora } from "@/components/aurora"; import { SearchBar } from "@/components/search-bar"; import { TrackRow } from "@/components/track-row"; import { AlbumView } from "@/components/album-view"; import { ArtistView } from "@/components/artist-view"; import { BulkImport } from "@/components/bulk-import"; import { Requests, type JobsSnapshot } from "@/components/requests"; import { Skeletons } from "@/components/skeletons"; import { PlayerProvider } from "@/components/player"; import { ToastProvider } from "@/components/toast"; import { AdminPanel } from "@/components/admin/admin-panel"; type Tab = "search" | "jobs" | "bulk" | "admin"; type View = | { k: "welcome" } | { k: "results"; q: string } | { k: "album"; id: number } | { k: "artist"; id: number }; const EXAMPLES = [ "Radiohead Creep", "Kendrick Lamar Money Trees", "Fleetwood Mac Dreams", "Tame Impala The Less I Know The Better", "Frank Ocean Ivy", ]; export default function Page() { return ( ); } function Muse() { const [tab, setTab] = React.useState("search"); const [view, setView] = React.useState({ k: "welcome" }); const [history, setHistory] = React.useState([]); const [results, setResults] = React.useState(null); const [busy, setBusy] = React.useState(false); const [me, setMe] = React.useState(null); const [snap, setSnap] = React.useState(null); const go = React.useCallback((next: View) => { setView((cur) => { setHistory((h) => (cur.k === "welcome" ? h : [...h, cur])); return next; }); setTab("search"); window.scrollTo({ top: 0, behavior: "smooth" }); }, []); const back = () => { setHistory((h) => { const prev = h[h.length - 1]; if (prev) setView(prev); return h.slice(0, -1); }); }; const doSearch = React.useCallback( async (q: string) => { if (!q.trim()) return; go({ k: "results", q }); setResults(null); setBusy(true); try { const r = await api.search(q); setResults(r.results); if (r.results[0]) void tint(r.results[0].cover); } catch { setResults([]); } setBusy(false); }, [go], ); const openAlbum = React.useCallback((id: number) => go({ k: "album", id }), [go]); const openArtist = React.useCallback((id: number) => go({ k: "artist", id }), [go]); const refresh = React.useCallback(async () => { try { setSnap(await api.jobs()); } catch { /* a 401 here just means the poll outlived the session */ } // Refetched alongside jobs rather than once at mount so the quota chip // counts down as you queue, and so a role change or a disable lands within // one poll instead of at next sign-in. try { setMe(await api.me()); } catch { /* ignore */ } }, []); React.useEffect(() => { void refresh(); const t = setInterval(refresh, 4000); return () => clearInterval(t); }, [refresh]); const active = (snap?.counts.queued ?? 0) + (snap?.counts.searching ?? 0) + (snap?.counts.downloading ?? 0); const isAdmin = me?.role === "admin"; // Only worth showing when a limit actually exists — a permanent "0 / 0" chip // is noise on a single-user install, which is most of them. const quotaLeft = me?.quota && me.usage ? Math.max(0, me.quota - me.usage.requests) : null; return ( <>

muse

search · request · it appears in Navidrome

{me?.user && (
{quotaLeft !== null && ( {quotaLeft} left )} {me.avatar && ( /* eslint-disable-next-line @next/next/no-img-element */ )} {me.display || me.user} {me.role && me.role !== "user" && ( {me.role} )} Log out
)}
{/* Keyed on `tab`, so React remounts and Framer replays the entrance. Deliberately NOT wrapped in AnimatePresence mode="wait": that holds the incoming panel until the outgoing one's exit animation reports finished, which makes navigation depend on an animation completing. Anything that stalls the frame loop — a backgrounded tab, a browser that throttles rAF — then leaves the UI stuck on the old panel with no way forward. A cosmetic transition should never gate navigation. */} {tab === "search" && ( <> {history.length > 0 && ( Back )} {view.k === "welcome" && } {view.k === "results" && (results === null ? ( ) : results.length === 0 ? (
No matches. Try adding the artist name.
) : ( {results.map((t, i) => ( ))} ))} {view.k === "album" && ( )} {view.k === "artist" && ( )} )} {tab === "jobs" && } {tab === "bulk" && } {tab === "admin" && isAdmin && }
); } function Welcome({ onPick }: { onPick: (q: string) => void }) { return (

Find anything

Search a song and press{" "} Enter . Hit play to hear a 30-second preview before you commit, then Get to pull it into your library, tagged.

{EXAMPLES.map((e, i) => ( onPick(e)} className="glass cursor-pointer rounded-full px-3.5 py-1.5 text-[13px] text-dim transition hover:border-accent-line hover:bg-accent-soft hover:text-ink" > {e} ))}
); }