Library import asks for a location to file the cards under
After picking a file, Import cards… asks where the cards are kept, pre-filled with the file's name (a ManaBox export is named after its binder). A location is set on every imported card, replacing any it had, as Add Cards does; blank leaves locations alone. It's applied inside the import's single transaction.
This commit is contained in:
@@ -101,6 +101,8 @@ export function LibraryBrowser({
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
// The "what can I import?" explainer shown before the file picker.
|
||||
const [importIntroOpen, setImportIntroOpen] = useState(false);
|
||||
// The file picked to import, while asking where its cards are kept.
|
||||
const [importPath, setImportPath] = useState<string | null>(null);
|
||||
// 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
|
||||
@@ -322,14 +324,18 @@ export function LibraryBrowser({
|
||||
title: "Import library cards",
|
||||
filters: [{ name: "Card list (decklist or ManaBox export)", extensions: ["txt", "csv"] }],
|
||||
});
|
||||
if (typeof path !== "string") return;
|
||||
if (typeof path === "string") setImportPath(path);
|
||||
}
|
||||
|
||||
async function runImport(path: string, location: string) {
|
||||
setImportPath(null);
|
||||
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));
|
||||
setImportResult(await api.importLibraryCards(path, location));
|
||||
} catch (err) {
|
||||
setImportError(String(err));
|
||||
} finally {
|
||||
@@ -603,7 +609,10 @@ export function LibraryBrowser({
|
||||
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>
|
||||
<p className="dialog-message">
|
||||
After you choose a file, you can file its cards under a location, like the binder they
|
||||
came from. 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>
|
||||
@@ -613,6 +622,14 @@ export function LibraryBrowser({
|
||||
</DialogFrame>
|
||||
)}
|
||||
|
||||
{importPath && (
|
||||
<ImportLocationDialog
|
||||
path={importPath}
|
||||
onImport={(location) => runImport(importPath, location)}
|
||||
onCancel={() => setImportPath(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{importProgress && showImportProgress && (
|
||||
<ProgressDialog
|
||||
title="Importing cards…"
|
||||
@@ -795,3 +812,60 @@ export function LibraryBrowser({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A picked file's name without its folder or extension — a ManaBox export
|
||||
* is named after its binder ("Green Bulk.csv"). Copes with Android content
|
||||
* URIs, whose last segment is a percent-encoded "primary:Download/x.csv". */
|
||||
function fileStem(path: string): string {
|
||||
let name = path;
|
||||
try {
|
||||
name = decodeURIComponent(path);
|
||||
} catch {
|
||||
// Not percent-encoded after all; use it as it is.
|
||||
}
|
||||
name = name.split(/[/\\:]/).pop() ?? "";
|
||||
return name.replace(/\.[^.]*$/, "").trim();
|
||||
}
|
||||
|
||||
/** Asks where an import's cards are kept, pre-filled with the file's name.
|
||||
* Blank imports them without touching any card's location. */
|
||||
function ImportLocationDialog({
|
||||
path,
|
||||
onImport,
|
||||
onCancel,
|
||||
}: {
|
||||
path: string;
|
||||
onImport: (location: string) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState(() => fileStem(path));
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => inputRef.current?.select(), []);
|
||||
const submit = () => onImport(value.trim());
|
||||
|
||||
return (
|
||||
<DialogFrame title="Where are these cards?" onCancel={onCancel}>
|
||||
<label className="dialog-field">
|
||||
<span className="muted">Location</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="e.g. Box 1"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && submit()}
|
||||
/>
|
||||
</label>
|
||||
<p className="dialog-message">
|
||||
Every imported card is filed under this location, replacing any it already had. Leave it
|
||||
blank to keep cards' current locations.
|
||||
</p>
|
||||
<div className="dialog-actions">
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
<button className="primary" onClick={submit}>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</DialogFrame>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user