Printing tracking for owned copies and deck art, and an outdated-card-data notice

Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.

A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.

UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.

Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.

Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.

reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
This commit is contained in:
2026-10-16 21:06:17 -05:00
parent 1c5e045591
commit 214a535588
23 changed files with 2092 additions and 138 deletions

View File

@@ -732,6 +732,10 @@ struct ScryPrinting {
/// Scryfall's set category; `"funny"` covers the Un-sets.
#[serde(default)]
set_type: String,
#[serde(default)]
lang: String,
#[serde(default)]
released_at: String,
}
#[derive(Deserialize)]
@@ -758,8 +762,8 @@ fn load_printings(conn: &Connection, reader: impl Read, cancel: &CancelToken) ->
{
let mut ins = conn.prepare(
"INSERT OR IGNORE INTO printings
(scryfall_id,oracle_id,set_code,collector_number,finishes,rarity,image_uri,back_image_uri,price_usd,universes_beyond,digital,funny)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
(scryfall_id,oracle_id,set_code,collector_number,finishes,rarity,image_uri,back_image_uri,price_usd,universes_beyond,digital,funny,lang,released_at)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14)",
)?;
stream_jsonl::<_, ScryPrinting, _>(reader, |p| {
if cancel.is_cancelled() {
@@ -814,6 +818,8 @@ fn load_printings(conn: &Connection, reader: impl Read, cancel: &CancelToken) ->
universes_beyond as i64,
digital as i64,
funny as i64,
p.lang,
p.released_at,
])?;
count += 1;
Ok(())
@@ -1036,6 +1042,7 @@ mod tests {
let printings = r#"[
{"id":"p-1","oracle_id":"oid-1","set":"m19","collector_number":"310",
"finishes":["nonfoil","foil"],"rarity":"common",
"lang":"en","released_at":"2018-07-13",
"image_uris":{"normal":"http://img/1"}}
]"#;
@@ -1067,6 +1074,16 @@ mod tests {
)
.unwrap();
assert_eq!(g as u8, crate::model::G);
// Picker labels: language and release date.
let (lang, released): (String, String) = conn
.query_row(
"SELECT lang, released_at FROM printings WHERE scryfall_id='p-1'",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!((lang.as_str(), released.as_str()), ("en", "2018-07-13"));
}
#[test]