Files
mtgc/src/App.tsx

171 lines
5.2 KiB
TypeScript
Raw Normal View History

import { useEffect, useState, useCallback, ReactNode } from "react";
import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import { api, 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) => (
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
);
const NAV: { id: Tab; label: string; icon: ReactNode }[] = [
{ id: "add", label: "Add Cards", icon: svg(<path d="M12 5v14M5 12h14" />) },
{
id: "library",
label: "Library",
icon: svg(
<>
<rect x="3" y="3" width="7" height="7" rx="1.5" />
<rect x="14" y="3" width="7" height="7" rx="1.5" />
<rect x="3" y="14" width="7" height="7" rx="1.5" />
<rect x="14" y="14" width="7" height="7" rx="1.5" />
</>,
),
},
{
id: "decks",
label: "Decks",
icon: svg(
<>
<path d="M12 2.5l8.5 4.5-8.5 4.5L3.5 7z" />
<path d="M3.5 12l8.5 4.5 8.5-4.5" />
<path d="M3.5 16.5l8.5 4.5 8.5-4.5" />
</>,
),
},
];
export default function App() {
const [status, setStatus] = useState<Status | null>(null);
const [tab, setTab] = usePersistedState<Tab>("active-tab", "add");
const [progress, setProgress] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const refreshStatus = useCallback(async () => {
setStatus(await api.getStatus());
}, []);
useEffect(() => {
refreshStatus();
const un = listen<string>("ingest-progress", (e) => setProgress(e.payload));
return () => {
un.then((f) => f());
};
}, [refreshStatus]);
async function updateCardData() {
setBusy(true);
setProgress("Starting…");
try {
const s = await api.updateCardData();
setStatus(s);
setProgress(`Loaded ${s.card_count.toLocaleString()} cards.`);
} catch (err) {
setProgress(`Failed: ${String(err)}`);
} finally {
setBusy(false);
}
}
async function chooseLibrary() {
const path = await open({
title: "Choose or create library.db (put it in a synced folder)",
filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }],
});
if (typeof path === "string") {
setStatus(await api.setLibraryPath(path));
}
}
const ready = status?.reference_ready ?? false;
return (
<HoverPreviewProvider>
<div className="app">
<nav className="nav-rail">
<div className="brand">
<span className="brand-mark">M</span>
<span className="brand-text">MTG Library</span>
</div>
<div className="nav-items">
{NAV.map((n) => (
<button
key={n.id}
className={`nav-item ${tab === n.id ? "active" : ""}`}
onClick={() => setTab(n.id)}
>
<span className="nav-icon">{n.icon}</span>
{n.label}
</button>
))}
</div>
<div className="nav-foot">
<span className={`status-dot ${ready ? "on" : "off"}`} />
{ready ? `${status?.card_count.toLocaleString()} cards` : "No card data"}
</div>
</nav>
<div className="main">
<header className="topbar">
{status && (
<span className="path" title={status.library_path}>
{status.library_path}
</span>
)}
<span className="spacer" />
{progress && <span className="progress">{progress}</span>}
<button onClick={chooseLibrary} disabled={busy}>
Choose library…
</button>
<button onClick={updateCardData} disabled={busy} className="primary">
{ready ? "Update card data" : "Download card data"}
</button>
</header>
<div className="content">
{!ready ? (
<div className="empty-state">
<div className="empty-mark">✦</div>
<h2>Welcome to your MTG library</h2>
<p className="hint">
No card database yet. Click <strong>Download card data</strong> 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.
</p>
<button className="primary big" onClick={updateCardData} disabled={busy}>
{busy ? "Downloading…" : "Download card data"}
</button>
</div>
) : tab === "add" ? (
<CardEntry />
) : tab === "library" ? (
<LibraryBrowser />
) : (
<DeckView />
)}
</div>
</div>
</div>
</HoverPreviewProvider>
);
}