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:
2026-10-17 21:58:44 -05:00
parent 1641d9aa47
commit 4b651b5108
6 changed files with 149 additions and 19 deletions

View File

@@ -456,15 +456,24 @@ impl Catalog {
/// Import a card list into the library: a plain-text list (ours, or
/// ManaBox's text export) or a ManaBox CSV export, told apart by
/// [`crate::manabox::is_manabox_csv`]. The whole import is one
/// transaction, so a failure part-way leaves the library as it was.
/// `on_progress(done, total)` is called after each line.
pub fn import_card_list(&self, text: &str, mut on_progress: impl FnMut(usize, usize)) -> Result<LibraryImport> {
/// [`crate::manabox::is_manabox_csv`]. A non-blank `location` becomes
/// the storage location of every card imported, replacing any it had,
/// as adding a card on Add Cards with a location does; a blank one
/// leaves locations alone. The whole import is one transaction, so a
/// failure part-way leaves the library as it was. `on_progress(done,
/// total)` is called after each line.
pub fn import_card_list(
&self,
text: &str,
location: &str,
mut on_progress: impl FnMut(usize, usize),
) -> Result<LibraryImport> {
let location = location.trim();
let tx = self.nested_tx()?;
let result = if crate::manabox::is_manabox_csv(text) {
self.import_manabox_rows(&crate::manabox::parse_manabox_csv(text)?, &mut on_progress)?
self.import_manabox_rows(&crate::manabox::parse_manabox_csv(text)?, location, &mut on_progress)?
} else {
self.import_library_lines(&parse_library_text(text), &mut on_progress)?
self.import_library_lines(&parse_library_text(text), location, &mut on_progress)?
};
tx.commit()?;
Ok(result)
@@ -479,14 +488,18 @@ impl Catalog {
/// new_total_quantity_owned)` per applied line, plus unresolved names.
pub fn import_library_list(&self, text: &str) -> Result<LibraryImport> {
let tx = self.nested_tx()?;
let result = self.import_library_lines(&parse_library_text(text), &mut |_, _| {})?;
let result = self.import_library_lines(&parse_library_text(text), "", &mut |_, _| {})?;
tx.commit()?;
Ok(result)
}
/// The import loop behind [`Catalog::import_card_list`] and
/// [`Catalog::import_library_list`]; `location` is set on each imported
/// card unless empty.
fn import_library_lines(
&self,
lines: &[DecklistLine],
location: &str,
on_progress: &mut dyn FnMut(usize, usize),
) -> Result<LibraryImport> {
let mut added = Vec::new();
@@ -498,6 +511,9 @@ impl Catalog {
continue;
};
let new_qty = self.add_owned(&oid, line.quantity)?;
if !location.is_empty() {
self.set_location(&oid, location)?;
}
if let Some(scryfall_id) = printing {
let wanted = line.printing.as_ref().and_then(|p| p.finish.clone());
self.add_imported_copies(&oid, &scryfall_id, wanted, line.quantity)?;
@@ -664,17 +680,41 @@ mod tests {
let cat = &fx.catalog;
let mut seen = Vec::new();
let (added, _) = cat
.import_card_list("2 Lightning Bolt\n1 Sol Ring\n", |done, total| seen.push((done, total)))
.import_card_list("2 Lightning Bolt\n1 Sol Ring\n", "", |done, total| seen.push((done, total)))
.unwrap();
assert_eq!(added.len(), 2);
assert_eq!(seen, vec![(0, 2), (1, 2), (2, 2)]);
let csv = "Name,Quantity,Scryfall ID\nSerra Angel,3,\n";
let (added, unresolved) = cat.import_card_list(csv, |_, _| {}).unwrap();
let (added, unresolved) = cat.import_card_list(csv, "", |_, _| {}).unwrap();
assert!(unresolved.is_empty());
assert_eq!(added, vec![("Serra Angel".to_string(), 3)]);
}
#[test]
fn import_card_list_files_imported_cards_under_a_location() {
let fx = test_support::make();
let cat = &fx.catalog;
cat.add_owned("sol-ring", 1).unwrap();
cat.set_location("sol-ring", "Box 1").unwrap();
cat.add_owned("serra-angel", 1).unwrap();
cat.set_location("serra-angel", "Box 1").unwrap();
let location = |oid: &str| {
cat.list_library().unwrap().into_iter().find(|c| c.card.oracle_id == oid).unwrap().location
};
// Blank leaves every location alone.
cat.import_card_list("1 Sol Ring\n", " ", |_, _| {}).unwrap();
assert_eq!(location("sol-ring"), "Box 1");
// Otherwise it's set on, and only on, the imported cards.
let csv = "Name,Quantity,Scryfall ID\nSol Ring,1,\nLightning Bolt,2,\n";
cat.import_card_list(csv, " Green Bulk ", |_, _| {}).unwrap();
assert_eq!(location("sol-ring"), "Green Bulk");
assert_eq!(location("lightning-bolt"), "Green Bulk");
assert_eq!(location("serra-angel"), "Box 1");
}
#[test]
fn writes_inside_an_uncommitted_nested_tx_are_rolled_back() {
let fx = test_support::make();

View File

@@ -180,14 +180,18 @@ impl Catalog {
/// [`Catalog::import_card_list`], which reports progress.
pub fn import_manabox_csv(&self, text: &str) -> Result<LibraryImport> {
let tx = self.nested_tx()?;
let result = self.import_manabox_rows(&parse_manabox_csv(text)?, &mut |_, _| {})?;
let result = self.import_manabox_rows(&parse_manabox_csv(text)?, "", &mut |_, _| {})?;
tx.commit()?;
Ok(result)
}
/// The import loop behind [`Catalog::import_manabox_csv`] and
/// [`Catalog::import_card_list`]; `location` is set on each imported
/// card unless empty.
pub(crate) fn import_manabox_rows(
&self,
rows: &[ManaboxRow],
location: &str,
on_progress: &mut dyn FnMut(usize, usize),
) -> Result<LibraryImport> {
let mut added = Vec::new();
@@ -199,6 +203,9 @@ impl Catalog {
continue;
};
let new_qty = self.add_owned(&oid, row.quantity)?;
if !location.is_empty() {
self.set_location(&oid, location)?;
}
if let Some(scryfall_id) = printing {
self.add_imported_copies(&oid, &scryfall_id, row.finish.clone(), row.quantity)?;
}