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:
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use mtgc_core::sync::account::{redeem_code, server_info, start_url, Pkce};
|
||||
use mtgc_core::sync::remote::{sync_cycle, HttpRemote, SyncBase, UNAUTHORIZED};
|
||||
use mtgc_core::sync::{apply_doc, merge_into, read_doc, CardEntry};
|
||||
use mtgc_core::sync::{apply_doc, merge_into, printing_key, read_doc, CardEntry};
|
||||
use mtgc_server::auth::local::hash_password;
|
||||
use mtgc_server::{db, router, AppState, Config};
|
||||
|
||||
@@ -75,16 +75,54 @@ impl Device {
|
||||
fn set(&self, id: &str, qty: i64) {
|
||||
let conn = mtgc_core::db::open_library(&self.db).unwrap();
|
||||
let mut doc = read_doc(&conn).unwrap();
|
||||
doc.cards.insert(id.into(), CardEntry { quantity: qty, location: String::new() });
|
||||
doc.cards.insert(id.into(), CardEntry { quantity: qty, ..Default::default() });
|
||||
apply_doc(&conn, &doc).unwrap();
|
||||
}
|
||||
|
||||
fn doc(&self) -> mtgc_core::sync::LibraryDoc {
|
||||
read_doc(&mtgc_core::db::open_library(&self.db).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn qty(&self, id: &str) -> Option<i64> {
|
||||
let conn = mtgc_core::db::open_library(&self.db).unwrap();
|
||||
read_doc(&conn).unwrap().cards.get(id).map(|c| c.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printing_data_reaches_the_other_device_intact() {
|
||||
// A version-2 document must be accepted by the server and come back out
|
||||
// exactly as stored, or one device's tracked printings would vanish.
|
||||
let (server, state) = spawn_server();
|
||||
let hash = hash_password("correct horse battery").unwrap();
|
||||
db::create_local_user(&state.db(), "bob", "Bob", &hash).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("mtgc-e2e-printings-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let phone_login = sign_in(&server, "bob", "correct horse battery");
|
||||
let desktop_login = sign_in(&server, "bob", "correct horse battery");
|
||||
let mut phone = Device::new(&dir, "phone", &server, &phone_login.token);
|
||||
let mut desktop = Device::new(&dir, "desktop", &server, &desktop_login.token);
|
||||
|
||||
{
|
||||
let conn = mtgc_core::db::open_library(&phone.db).unwrap();
|
||||
let mut doc = read_doc(&conn).unwrap();
|
||||
let mut card = CardEntry { quantity: 3, ..Default::default() };
|
||||
card.printings.insert(printing_key("p1", "foil"), 2);
|
||||
doc.cards.insert("a".into(), card);
|
||||
apply_doc(&conn, &doc).unwrap();
|
||||
}
|
||||
assert_eq!(phone.doc().version, 2);
|
||||
assert!(phone.sync().unwrap().published);
|
||||
desktop.sync().unwrap();
|
||||
|
||||
assert_eq!(desktop.doc(), phone.doc());
|
||||
assert_eq!(desktop.doc().cards["a"].printings[&printing_key("p1", "foil")], 2);
|
||||
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_devices_converge_through_the_real_server() {
|
||||
let (server, state) = spawn_server();
|
||||
|
||||
Reference in New Issue
Block a user