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

@@ -434,10 +434,34 @@ pub fn export_library_cards(db: Db, oracle_ids: Vec<String>, path: String) -> Re
std::fs::write(&path, text).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn import_library_cards(db: Db, path: String) -> Res<LibraryImportResult> {
let s = db.lock().unwrap();
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let (added, unresolved) = s.catalog()?.import_library_list(&text).map_err(|e| e.to_string())?;
Ok(LibraryImportResult { added, unresolved })
/// How far a library import has got, for the progress bar.
#[derive(Clone, Serialize)]
pub struct LibraryImportProgress {
pub done: usize,
pub total: usize,
}
/// Import a card list (plain text or a ManaBox export) into the library.
/// Runs on a blocking thread so the UI stays responsive, emitting
/// `library-import-progress` as it goes.
#[tauri::command]
pub async fn import_library_cards(app: AppHandle, path: String) -> Res<LibraryImportResult> {
crate::sync::blocking(move || {
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let s = crate::sync::lock(&app);
// At most one event per percent, so a big file doesn't flood the UI.
let mut last_percent = None;
let (added, unresolved) = s
.catalog()?
.import_card_list(&text, |done, total| {
let percent = (done * 100).checked_div(total).unwrap_or(100);
if last_percent != Some(percent) {
last_percent = Some(percent);
let _ = app.emit("library-import-progress", LibraryImportProgress { done, total });
}
})
.map_err(|e| e.to_string())?;
Ok(LibraryImportResult { added, unresolved })
})
.await
}