2026-07-07 20:22:31 -05:00
|
|
|
import { useEffect, useRef, useState, useCallback, ReactNode } from "react";
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
import { listen } from "@tauri-apps/api/event";
|
2026-07-07 20:22:31 -05:00
|
|
|
import { open, save } from "@tauri-apps/plugin-dialog";
|
|
|
|
|
import { api, IngestProgress, IngestStageInfo, Status } from "./api";
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
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" />
|
|
|
|
|
</>,
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
export default function App() {
|
|
|
|
|
const [status, setStatus] = useState<Status | null>(null);
|
|
|
|
|
const [tab, setTab] = usePersistedState<Tab>("active-tab", "add");
|
2026-07-07 20:22:31 -05:00
|
|
|
const [progress, setProgress] = useState<IngestProgress | null>(null);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
const [busy, setBusy] = useState(false);
|
2026-07-07 20:22:31 -05:00
|
|
|
const lastProgress = useRef<IngestProgress | null>(null);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
const refreshStatus = useCallback(async () => {
|
|
|
|
|
setStatus(await api.getStatus());
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
refreshStatus();
|
2026-07-07 20:22:31 -05:00
|
|
|
const un = listen<IngestProgress>("ingest-progress", (e) => {
|
|
|
|
|
lastProgress.current = e.payload;
|
|
|
|
|
setProgress(e.payload);
|
|
|
|
|
});
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
return () => {
|
|
|
|
|
un.then((f) => f());
|
|
|
|
|
};
|
|
|
|
|
}, [refreshStatus]);
|
|
|
|
|
|
|
|
|
|
async function updateCardData() {
|
|
|
|
|
setBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
const s = await api.updateCardData();
|
|
|
|
|
setStatus(s);
|
2026-07-07 20:22:31 -05:00
|
|
|
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);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
} catch (err) {
|
2026-07-07 20:22:31 -05:00
|
|
|
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 ?? [],
|
|
|
|
|
});
|
|
|
|
|
}
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
} finally {
|
|
|
|
|
setBusy(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
async function cancelUpdate() {
|
|
|
|
|
await api.cancelUpdateCardData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openLibrary() {
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
const path = await open({
|
2026-07-07 20:22:31 -05:00
|
|
|
title: "Open an existing library.db",
|
|
|
|
|
filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }],
|
|
|
|
|
});
|
|
|
|
|
if (typeof path === "string") {
|
|
|
|
|
setStatus(await api.setLibraryPath(path));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function newLibrary() {
|
|
|
|
|
const path = await save({
|
|
|
|
|
title: "Create a new library.db (put it in a synced folder)",
|
|
|
|
|
defaultPath: "library.db",
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
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" />
|
2026-07-07 20:22:31 -05:00
|
|
|
<button onClick={openLibrary} disabled={busy}>
|
|
|
|
|
Open Library…
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={newLibrary} disabled={busy}>
|
|
|
|
|
New Library…
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
</button>
|
|
|
|
|
<button onClick={updateCardData} disabled={busy} className="primary">
|
|
|
|
|
{ready ? "Update card data" : "Download card data"}
|
|
|
|
|
</button>
|
|
|
|
|
</header>
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
{progress && (
|
|
|
|
|
<div className={`ingest-bar ingest-bar-${progress.stage}`}>
|
|
|
|
|
<div className="ingest-bar-head">
|
|
|
|
|
<span className="ingest-bar-message">{progress.message}</span>
|
|
|
|
|
<span className="spacer" />
|
|
|
|
|
{busy && (
|
|
|
|
|
<button className="ingest-bar-cancel" onClick={cancelUpdate}>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{!busy &&
|
|
|
|
|
(progress.stage === "done" ||
|
|
|
|
|
progress.stage === "error" ||
|
|
|
|
|
progress.stage === "cancelled") && (
|
|
|
|
|
<button
|
|
|
|
|
className="ingest-bar-dismiss"
|
|
|
|
|
onClick={() => setProgress(null)}
|
|
|
|
|
title="Dismiss"
|
|
|
|
|
>
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ingest-segments">
|
|
|
|
|
{progress.stages.map((s, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={s.id}
|
|
|
|
|
className={`ingest-segment ${s.id === progress.stage ? "active" : ""}`}
|
|
|
|
|
style={{ flexGrow: s.weight, flexBasis: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<div className="ingest-segment-track">
|
|
|
|
|
<div
|
|
|
|
|
className="ingest-segment-fill"
|
|
|
|
|
style={{ width: `${segmentFill(progress.stages, i, progress.overall_percent)}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="ingest-segment-label">{s.label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|