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:
@@ -1,3 +1,4 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { open, save } from "@tauri-apps/plugin-dialog";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@@ -12,12 +13,14 @@ import {
|
||||
import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
DialogFrame,
|
||||
ColorPips,
|
||||
ColorToggles,
|
||||
GroupBySelect,
|
||||
GroupMode,
|
||||
InfoDialog,
|
||||
LocationField,
|
||||
ProgressDialog,
|
||||
PromptDialog,
|
||||
QtyStepper,
|
||||
SortBySelect,
|
||||
@@ -95,6 +98,14 @@ export function LibraryBrowser({
|
||||
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
const [importResult, setImportResult] = useState<LibraryImportResult | null>(null);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
// The "what can I import?" explainer shown before the file picker.
|
||||
const [importIntroOpen, setImportIntroOpen] = useState(false);
|
||||
// Lines done / total while an import runs; null when idle.
|
||||
const [importProgress, setImportProgress] = useState<{ done: number; total: number } | null>(null);
|
||||
// Only show the progress dialog for an import that's taking a while, so a
|
||||
// quick one doesn't flash it up.
|
||||
const [showImportProgress, setShowImportProgress] = useState(false);
|
||||
const selectAllRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -306,14 +317,28 @@ export function LibraryBrowser({
|
||||
}
|
||||
|
||||
async function pickImportFile() {
|
||||
setImportIntroOpen(false);
|
||||
const path = await open({
|
||||
title: "Import library cards",
|
||||
filters: [{ name: "Decklist", extensions: ["txt"] }],
|
||||
filters: [{ name: "Card list (decklist or ManaBox export)", extensions: ["txt", "csv"] }],
|
||||
});
|
||||
if (typeof path !== "string") return;
|
||||
const result = await api.importLibraryCards(path);
|
||||
setImportResult(result);
|
||||
setRefreshKey((k) => k + 1);
|
||||
setImportProgress({ done: 0, total: 0 });
|
||||
const reveal = setTimeout(() => setShowImportProgress(true), 300);
|
||||
const unlisten = await listen<{ done: number; total: number }>("library-import-progress", (e) =>
|
||||
setImportProgress(e.payload),
|
||||
);
|
||||
try {
|
||||
setImportResult(await api.importLibraryCards(path));
|
||||
} catch (err) {
|
||||
setImportError(String(err));
|
||||
} finally {
|
||||
unlisten();
|
||||
clearTimeout(reveal);
|
||||
setShowImportProgress(false);
|
||||
setImportProgress(null);
|
||||
setRefreshKey((k) => k + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const deckOrLocation = (c: LibraryCard) =>
|
||||
@@ -455,7 +480,12 @@ export function LibraryBrowser({
|
||||
Library <span className="badge">{rows.length}</span>
|
||||
</h2>
|
||||
<div className="page-header-actions">
|
||||
<button onClick={pickImportFile}>Import cards…</button>
|
||||
<button
|
||||
onClick={() => setImportIntroOpen(true)}
|
||||
disabled={importProgress !== null}
|
||||
>
|
||||
Import cards…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -554,6 +584,52 @@ export function LibraryBrowser({
|
||||
/>
|
||||
)}
|
||||
|
||||
{importIntroOpen && (
|
||||
<DialogFrame title="Import cards" onCancel={() => setImportIntroOpen(false)} wide>
|
||||
<p className="dialog-message">
|
||||
Adds the cards in a file to your library. Nothing you already own is replaced or
|
||||
removed. You can import:
|
||||
</p>
|
||||
<ul className="dialog-list">
|
||||
<li>
|
||||
<strong>A card list</strong> (.txt), one card per line, like <code>4 Lightning Bolt</code>.
|
||||
A line can name a printing, like <code>1 Sol Ring (2X2) 315 *F*</code>, where{" "}
|
||||
<code>*F*</code> is foil and <code>*E*</code> etched. Library export, Moxfield and
|
||||
Arena all write this format.
|
||||
</li>
|
||||
<li>
|
||||
<strong>A ManaBox export</strong> of a collection or binder, either the .csv or the
|
||||
.txt. The CSV records each copy's exact printing and finish, so it's the better
|
||||
choice. ManaBox's condition, language and price fields aren't kept.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="dialog-message">Any cards that can't be matched are listed afterwards.</p>
|
||||
<div className="dialog-actions">
|
||||
<button onClick={() => setImportIntroOpen(false)}>Cancel</button>
|
||||
<button className="primary" onClick={pickImportFile} autoFocus>
|
||||
Choose file…
|
||||
</button>
|
||||
</div>
|
||||
</DialogFrame>
|
||||
)}
|
||||
|
||||
{importProgress && showImportProgress && (
|
||||
<ProgressDialog
|
||||
title="Importing cards…"
|
||||
done={importProgress.done}
|
||||
total={importProgress.total}
|
||||
unit="lines"
|
||||
/>
|
||||
)}
|
||||
|
||||
{importError && (
|
||||
<InfoDialog
|
||||
title="Couldn't import that file"
|
||||
message={importError}
|
||||
onClose={() => setImportError(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{importResult && (
|
||||
<InfoDialog
|
||||
title="Cards imported"
|
||||
|
||||
Reference in New Issue
Block a user