import { useEffect, useState, useCallback } from "react"; import { apiGet } from "../../api"; import { PageHead, Empty, Loading, fmtDuration } from "../../components/ui"; import { IconDisc, IconMusic, IconSearch, IconPlay } from "../../components/icons"; import { useToast } from "../../lib/toast"; interface Album { id: string; name: string; artist: string; year?: number; song_count: number; duration: number; cover_url: string | null; } interface Song { id: string; title: string; track?: number; duration: number; artist: string; } interface AlbumDetail extends Album { songs: Song[]; } const SORTS = [ { key: "alphabeticalByName", label: "A–Z" }, { key: "newest", label: "Newest" }, { key: "recent", label: "Recently Played" }, { key: "frequent", label: "Most Played" }, { key: "random", label: "Random" }, ]; export default function Library() { const toast = useToast(); const [status, setStatus] = useState<{ connected: boolean; configured: boolean; error?: string } | null>(null); const [albums, setAlbums] = useState([]); const [loading, setLoading] = useState(true); const [sort, setSort] = useState("alphabeticalByName"); const [query, setQuery] = useState(""); const [selected, setSelected] = useState(null); const [detailLoading, setDetailLoading] = useState(false); useEffect(() => { apiGet("/api/navidrome/status").then(setStatus).catch(() => setStatus({ connected: false, configured: false })); }, []); const load = useCallback(() => { setLoading(true); const q = query.trim(); const path = q ? `/api/navidrome/albums?q=${encodeURIComponent(q)}&size=120` : `/api/navidrome/albums?type=${sort}&size=120`; apiGet<{ items: Album[] }>(path) .then((d) => setAlbums(d.items)) .catch((e) => toast(e.message, "err")) .finally(() => setLoading(false)); }, [sort, query, toast]); useEffect(() => { if (status?.connected) load(); }, [status?.connected, sort]); // eslint-disable-line react-hooks/exhaustive-deps function openAlbum(id: string) { setDetailLoading(true); setSelected(null); apiGet(`/api/navidrome/album/${id}`) .then(setSelected) .catch((e) => toast(e.message, "err")) .finally(() => setDetailLoading(false)); } if (status && !status.configured) { return ( <> Browse your Navidrome library.
}> Navidrome is not configured. Set NAVIDROME_URL, NAVIDROME_USER and{" "} NAVIDROME_PASSWORD and restart the app.
); } if (status && status.configured && !status.connected) { return ( <> Browse your Navidrome library.
}>Could not connect to Navidrome. {status.error}
); } return ( <> Browse artists and albums served by your Navidrome instance.
{SORTS.map((s) => ( ))}
{ e.preventDefault(); load(); }} > setQuery(e.target.value)} />
{loading ? ( ) : albums.length === 0 ? (
}>No albums found.
) : (
{albums.map((a) => (
openAlbum(a.id)}> {a.cover_url ? ( {a.name} ) : (
)}
{a.name}
{a.artist} {a.year ? ` · ${a.year}` : ""}
))}
)} {(selected || detailLoading) && (
setSelected(null)} >
e.stopPropagation()}> {detailLoading || !selected ? ( ) : ( <>
{selected.cover_url && ( )}

{selected.name}

{selected.artist} {selected.year ? ` · ${selected.year}` : ""} · {selected.song_count} tracks ·{" "} {fmtDuration(selected.duration)}
{selected.songs.map((s) => ( ))}
{s.track ?? } {s.title} {fmtDuration(s.duration)}
)}
)} ); }