import { useEffect, useRef, useState, useCallback, ReactNode } from "react"; import { listen } from "@tauri-apps/api/event"; import { open, save } from "@tauri-apps/plugin-dialog"; import { api, IngestProgress, IngestStageInfo, Status } from "./api"; import { HoverPreviewProvider, usePersistedState } from "./components"; import { CardEntry } from "./views/CardEntry"; import { LibraryBrowser } from "./views/LibraryBrowser"; import { DeckView } from "./views/DeckView"; type Tab = "add" | "library" | "decks"; const svg = (children: ReactNode, size = 20) => ( {children} ); const ICON_OPEN_LIBRARY = svg( , 16, ); const ICON_NEW_LIBRARY = svg( <> >, 16, ); const ICON_DOWNLOAD = svg( <> >, 16, ); const NAV: { id: Tab; label: string; icon: ReactNode }[] = [ { id: "add", label: "Add Cards", icon: svg() }, { id: "library", label: "Library", icon: svg( <> >, ), }, { id: "decks", label: "Decks", icon: svg( <> >, ), }, ]; /** Percent-fill (0..100) of one segment, given overall cumulative percent. */ function segmentFill(stages: IngestStageInfo[], index: number, overallPercent: number): number { let start = 0; for (let i = 0; i < index; i++) start += stages[i].weight; const end = start + stages[index].weight; const [startPct, endPct] = [start * 100, end * 100]; if (overallPercent <= startPct) return 0; if (overallPercent >= endPct) return 100; if (endPct === startPct) return 100; return ((overallPercent - startPct) / (endPct - startPct)) * 100; } export default function App() { const [status, setStatus] = useState(null); const [tab, setTab] = usePersistedState("active-tab", "add"); const [progress, setProgress] = useState(null); const [busy, setBusy] = useState(false); const lastProgress = useRef(null); const refreshStatus = useCallback(async () => { setStatus(await api.getStatus()); }, []); useEffect(() => { refreshStatus(); const un = listen("ingest-progress", (e) => { lastProgress.current = e.payload; setProgress(e.payload); }); return () => { un.then((f) => f()); }; }, [refreshStatus]); async function updateCardData() { setBusy(true); try { const s = await api.updateCardData(); setStatus(s); setProgress({ stage: "done", message: `Loaded ${s.card_count.toLocaleString()} cards.`, overall_percent: 100, stages: lastProgress.current?.stages ?? [], }); setTimeout(() => setProgress((p) => (p?.stage === "done" ? null : p)), 4000); } catch (err) { const msg = String(err); if (msg === "cancelled") { setProgress({ stage: "cancelled", message: "Cancelled.", overall_percent: lastProgress.current?.overall_percent ?? 0, stages: lastProgress.current?.stages ?? [], }); setTimeout(() => setProgress((p) => (p?.stage === "cancelled" ? null : p)), 3000); } else { setProgress({ stage: "error", message: `Failed: ${msg}`, overall_percent: lastProgress.current?.overall_percent ?? 0, stages: lastProgress.current?.stages ?? [], }); } } finally { setBusy(false); } } async function cancelUpdate() { await api.cancelUpdateCardData(); } async function switchLibrary(path: string) { try { setStatus(await api.setLibraryPath(path)); } catch (err) { setProgress({ stage: "error", message: `Couldn't open that library: ${String(err)}`, overall_percent: 0, stages: lastProgress.current?.stages ?? [], }); } } async function openLibrary() { const path = await open({ title: "Open an existing library.db", filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }], }); if (typeof path === "string") await switchLibrary(path); } async function newLibrary() { const path = await save({ title: "Create a new library.db (put it in a synced folder)", defaultPath: "library.db", filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }], }); if (typeof path === "string") await switchLibrary(path); } const ready = status?.reference_ready ?? false; return ( M MTG Library {NAV.map((n) => ( setTab(n.id)} > {n.icon} {n.label} ))} {ready ? `${status?.card_count.toLocaleString()} cards` : "No card data"} {status?.library_path ?? ""} {ICON_OPEN_LIBRARY} Open Library {ICON_NEW_LIBRARY} New Library {ICON_DOWNLOAD} {ready ? "Update card data" : "Download card data"} {progress && ( {progress.message} {busy && ( Cancel )} {!busy && (progress.stage === "done" || progress.stage === "error" || progress.stage === "cancelled") && ( setProgress(null)} title="Dismiss" > ✕ )} {progress.stages.map((s, i) => ( {s.label} ))} )} {!ready ? ( ✦ Welcome to your MTG library No card database yet. Click Download card data to fetch the latest cards from Scryfall (a few hundred MB, about a minute). The reference data stays on this device — only your library file is meant to be synced. {ICON_DOWNLOAD} {busy ? "Downloading…" : "Download card data"} ) : tab === "add" ? ( ) : tab === "library" ? ( ) : ( )} ); }
No card database yet. Click Download card data to fetch the latest cards from Scryfall (a few hundred MB, about a minute). The reference data stays on this device — only your library file is meant to be synced.