ManaBox import, with progress and a format explainer

Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.

A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.

Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
This commit is contained in:
2026-10-16 21:57:52 -05:00
parent 81b1df8c9f
commit 507df81f5f
10 changed files with 715 additions and 37 deletions

View File

@@ -301,6 +301,38 @@ export function InfoDialog({
);
}
/** Modal progress for work that can't be cancelled or dismissed (e.g. a
* library import): a title, a bar, and "N of M <unit>". `total` 0 shows an
* empty bar while the work is still starting. */
export function ProgressDialog({
title,
done,
total,
unit,
}: {
title: string;
done: number;
total: number;
unit: string;
}) {
const percent = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<div className="dialog-backdrop">
<div className="dialog" role="progressbar" aria-valuenow={percent} aria-valuemin={0} aria-valuemax={100}>
<h3 className="dialog-title">{title}</h3>
<div className="ingest-segment active progress-dialog-bar">
<div className="ingest-segment-track">
<div className="ingest-segment-fill" style={{ width: `${percent}%` }} />
</div>
</div>
<p className="dialog-message muted">
{total > 0 ? `${done.toLocaleString()} of ${total.toLocaleString()} ${unit}` : "Starting…"}
</p>
</div>
</div>
);
}
/** Floating snackbar offering to reverse the caller's last action. Auto-
* dismisses on a timer managed by the caller (this component is purely
* presentational) — pass `null`/omit rendering it once the window lapses. */