Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
//! Shared domain types used across the core.
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
/// A card's color identity / colors, encoded as a 5-bit WUBRG mask.
|
|
|
|
|
///
|
|
|
|
|
/// Bit layout (little end first): W=1, U=2, B=4, R=8, G=16.
|
|
|
|
|
/// Storing colors this way turns commander color-identity compatibility into a
|
|
|
|
|
/// single integer predicate (see [`ColorSet::is_subset_of`]).
|
|
|
|
|
pub const W: u8 = 1 << 0;
|
|
|
|
|
pub const U: u8 = 1 << 1;
|
|
|
|
|
pub const B: u8 = 1 << 2;
|
|
|
|
|
pub const R: u8 = 1 << 3;
|
|
|
|
|
pub const G: u8 = 1 << 4;
|
|
|
|
|
|
|
|
|
|
/// The five colors in canonical WUBRG order, paired with their single-letter code.
|
|
|
|
|
pub const WUBRG: [(u8, char); 5] = [(W, 'W'), (U, 'U'), (B, 'B'), (R, 'R'), (G, 'G')];
|
|
|
|
|
|
|
|
|
|
/// A set of colors as a WUBRG bitmask.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct ColorSet(pub u8);
|
|
|
|
|
|
|
|
|
|
impl ColorSet {
|
|
|
|
|
pub const COLORLESS: ColorSet = ColorSet(0);
|
|
|
|
|
|
|
|
|
|
/// Build from an iterator of single-letter color codes (e.g. Scryfall's
|
|
|
|
|
/// `["B", "G"]`). Unknown letters are ignored.
|
|
|
|
|
pub fn from_letters<I, S>(letters: I) -> Self
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = S>,
|
|
|
|
|
S: AsRef<str>,
|
|
|
|
|
{
|
|
|
|
|
let mut mask = 0u8;
|
|
|
|
|
for l in letters {
|
|
|
|
|
match l.as_ref() {
|
|
|
|
|
"W" | "w" => mask |= W,
|
|
|
|
|
"U" | "u" => mask |= U,
|
|
|
|
|
"B" | "b" => mask |= B,
|
|
|
|
|
"R" | "r" => mask |= R,
|
|
|
|
|
"G" | "g" => mask |= G,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ColorSet(mask)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse from a compact string like "BG" or "wubrg". Order-insensitive.
|
|
|
|
|
pub fn parse(s: &str) -> Self {
|
|
|
|
|
Self::from_letters(s.chars().map(|c| c.to_string()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Canonical WUBRG string, e.g. `ColorSet(B|G)` -> "BG". Colorless -> "".
|
|
|
|
|
pub fn to_letters(self) -> String {
|
|
|
|
|
WUBRG
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(bit, _)| self.0 & bit != 0)
|
|
|
|
|
.map(|(_, ch)| *ch)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_colorless(self) -> bool {
|
|
|
|
|
self.0 == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn contains(self, bit: u8) -> bool {
|
|
|
|
|
self.0 & bit != 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// True if every color in `self` is also in `other`.
|
|
|
|
|
///
|
|
|
|
|
/// This is the commander-legality rule: a card is playable in a commander of
|
|
|
|
|
/// color identity `C` iff `card_identity.is_subset_of(C)`.
|
|
|
|
|
pub fn is_subset_of(self, other: ColorSet) -> bool {
|
|
|
|
|
self.0 & !other.0 == 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
/// The 8 formats this app tracks legality for, in a fixed order that defines
|
|
|
|
|
/// their bit position within [`Legalities`] (2 bits each, in order). This
|
|
|
|
|
/// order must never change once shipped — it's baked into every ingested
|
|
|
|
|
/// reference.db. `.0` is the Scryfall `legalities` JSON key; `.1` is the
|
|
|
|
|
/// display label used throughout the app (matches DeckView's
|
|
|
|
|
/// `FORMATS`/`COMMANDER_FORMATS`).
|
|
|
|
|
pub const LEGALITY_FORMATS: [(&str, &str); 8] = [
|
|
|
|
|
("standard", "Standard"),
|
|
|
|
|
("pioneer", "Pioneer"),
|
|
|
|
|
("modern", "Modern"),
|
|
|
|
|
("legacy", "Legacy"),
|
|
|
|
|
("vintage", "Vintage"),
|
|
|
|
|
("pauper", "Pauper"),
|
|
|
|
|
("commander", "Commander"),
|
|
|
|
|
("brawl", "Brawl"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum LegalStatus {
|
|
|
|
|
NotLegal,
|
|
|
|
|
Legal,
|
|
|
|
|
Banned,
|
|
|
|
|
Restricted,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Per-format legality, encoded as 2 bits/format x 8 formats packed into one
|
|
|
|
|
/// i64 (see [`LEGALITY_FORMATS`] for bit-position order). Mirrors
|
|
|
|
|
/// `ColorSet`'s bitmask-newtype pattern but with 2-bit fields instead of
|
|
|
|
|
/// 1-bit flags, since each format has 4 possible statuses (not-legal / legal
|
|
|
|
|
/// / banned / restricted), not just a binary flag.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct Legalities(pub i64);
|
|
|
|
|
|
|
|
|
|
impl Legalities {
|
|
|
|
|
/// Build from Scryfall's `legalities` object (format-slug -> status-string
|
|
|
|
|
/// map). Any format not present in `map`, or present with an unrecognized
|
|
|
|
|
/// status string, decodes as `NotLegal`.
|
|
|
|
|
pub fn from_scryfall(map: &std::collections::HashMap<String, String>) -> Self {
|
|
|
|
|
let mut bits = 0i64;
|
|
|
|
|
for (i, (key, _label)) in LEGALITY_FORMATS.iter().enumerate() {
|
|
|
|
|
let status: i64 = match map.get(*key).map(String::as_str) {
|
|
|
|
|
Some("legal") => 1,
|
|
|
|
|
Some("banned") => 2,
|
|
|
|
|
Some("restricted") => 3,
|
|
|
|
|
_ => 0, // "not_legal", or key missing entirely
|
|
|
|
|
};
|
|
|
|
|
bits |= status << (i * 2);
|
|
|
|
|
}
|
|
|
|
|
Legalities(bits)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode the status for a given format's display label (e.g.
|
|
|
|
|
/// "Commander", "Modern" — matches DeckView's `FORMATS` strings).
|
|
|
|
|
/// Unknown labels decode as `NotLegal`.
|
|
|
|
|
pub fn status_for(self, format_label: &str) -> LegalStatus {
|
|
|
|
|
let Some(i) = LEGALITY_FORMATS.iter().position(|(_, label)| *label == format_label) else {
|
|
|
|
|
return LegalStatus::NotLegal;
|
|
|
|
|
};
|
|
|
|
|
match (self.0 >> (i * 2)) & 0b11 {
|
|
|
|
|
1 => LegalStatus::Legal,
|
|
|
|
|
2 => LegalStatus::Banned,
|
|
|
|
|
3 => LegalStatus::Restricted,
|
|
|
|
|
_ => LegalStatus::NotLegal,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
/// A card as returned by search / library queries. Oracle-level (one per
|
|
|
|
|
/// `oracle_id`), enriched with the caller's ownership + deck context where relevant.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct CardRef {
|
|
|
|
|
pub oracle_id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub mana_value: f64,
|
|
|
|
|
pub colors: ColorSet,
|
|
|
|
|
pub color_identity: ColorSet,
|
|
|
|
|
pub type_line: String,
|
2026-07-22 20:55:17 -05:00
|
|
|
/// Whether this card is printed/displayed sideways (Scryfall layout
|
|
|
|
|
/// "battle" or "planar" — Battles, Planes, and Phenomena). Its stored
|
|
|
|
|
/// image is itself portrait-shaped with the content rotated, so viewers
|
|
|
|
|
/// must rotate it 90° to display right-side up.
|
|
|
|
|
pub horizontal: bool,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
/// URL of a card image (Scryfall "normal" size) for one of its printings,
|
|
|
|
|
/// or empty if none is available.
|
|
|
|
|
pub image_uri: String,
|
2026-07-07 20:54:55 -05:00
|
|
|
/// URL of the *back face* image for the same printing (transform / modal
|
|
|
|
|
/// double-faced / reversible cards), or empty for single-faced cards.
|
|
|
|
|
pub back_image_uri: String,
|
2026-07-08 23:54:47 -05:00
|
|
|
/// WUBRG bitmask of colors this card can *produce* (lands, mana rocks,
|
|
|
|
|
/// dorks, etc.) — independent of `colors`/`color_identity`, which
|
|
|
|
|
/// describe what color the card *is*, not what it taps for.
|
|
|
|
|
pub produced_mana: ColorSet,
|
|
|
|
|
/// Whether this card can produce colorless mana (Scryfall's
|
|
|
|
|
/// `produced_mana` including "C"), tracked separately since colorless
|
|
|
|
|
/// isn't a WUBRG bit.
|
|
|
|
|
pub produces_colorless: bool,
|
2026-07-14 21:32:25 -05:00
|
|
|
/// USD market price (Scryfall's `prices.usd`) for the same printing that
|
|
|
|
|
/// supplied `image_uri`, or 0 if unknown.
|
|
|
|
|
pub price_usd: f64,
|
2026-07-20 00:21:22 -05:00
|
|
|
/// Per-format legality (banned/restricted/legal). See
|
|
|
|
|
/// [`LEGALITY_FORMATS`] for the bit-position order and
|
|
|
|
|
/// [`Legalities::status_for`] to decode a specific format.
|
|
|
|
|
pub legalities: Legalities,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A library row: a card the user owns, with quantity and its deck memberships.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LibraryCard {
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
pub card: CardRef,
|
|
|
|
|
pub quantity_owned: i64,
|
|
|
|
|
/// Names of decks this card is currently assigned to.
|
|
|
|
|
pub decks: Vec<String>,
|
2026-10-26 22:35:41 +00:00
|
|
|
/// Copies claimed by building/built decks' main boards (idea and retired
|
|
|
|
|
/// decks claim nothing). If it exceeds `quantity_owned`, the physical
|
|
|
|
|
/// copies are over-committed.
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
pub quantity_assigned: i64,
|
|
|
|
|
/// Free-text physical storage location (e.g. "Box 1"). Meaningful mainly for
|
|
|
|
|
/// cards not currently in a deck.
|
|
|
|
|
pub location: String,
|
2026-10-26 22:35:41 +00:00
|
|
|
/// The subset of `decks` that physically exist (building or built).
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub physical_decks: Vec<String>,
|
|
|
|
|
/// Copies physically pulled into decks; the rest are at `location`.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub quantity_in_decks: i64,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A card in a deck, whether or not the user owns it.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct DeckEntry {
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
pub card: CardRef,
|
|
|
|
|
/// Copies of this card in the deck.
|
|
|
|
|
pub quantity: i64,
|
|
|
|
|
pub is_commander: bool,
|
2026-07-14 21:32:25 -05:00
|
|
|
/// On the maybeboard: tracked against the deck for consideration, but
|
|
|
|
|
/// excluded from deck analysis stats and library assignment totals —
|
|
|
|
|
/// it isn't actually part of the deck.
|
|
|
|
|
pub is_maybeboard: bool,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
/// Copies owned in the library (0 if the card isn't in the library).
|
|
|
|
|
pub quantity_owned: i64,
|
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.
2026-10-16 21:06:17 -05:00
|
|
|
/// The printing whose art this deck slot shows, if one was chosen. The
|
|
|
|
|
/// card's `image_uri`/`back_image_uri` already reflect it (falling back
|
|
|
|
|
/// to the default when the id no longer resolves).
|
|
|
|
|
pub preferred_scryfall_id: Option<String>,
|
2026-10-23 22:29:56 -05:00
|
|
|
/// The card's partner-style ability, which lets it share the command
|
|
|
|
|
/// zone with a second commander.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub partner: Option<crate::partner::Partner>,
|
2026-10-26 22:35:41 +00:00
|
|
|
/// Copies physically in this deck; the rest are still to be pulled. Only
|
|
|
|
|
/// meaningful for building/built decks.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub quantity_pulled: i64,
|
|
|
|
|
/// Owned copies not claimed by any *other* building/built deck.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub quantity_spare: i64,
|
|
|
|
|
/// Owned copies not pulled into any deck — still in their box.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub quantity_in_box: i64,
|
|
|
|
|
/// The *other* building/built decks claiming copies — those with copies
|
|
|
|
|
/// pulled are where to take one from when none are left in the box.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub claimed_by: Vec<DeckHolding>,
|
|
|
|
|
/// Where the card's library copies are stored (e.g. "Box 1").
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub location: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A building/built deck's claim on a card's copies.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct DeckHolding {
|
|
|
|
|
pub deck_id: i64,
|
|
|
|
|
pub name: String,
|
|
|
|
|
/// Copies the deck's main board lists.
|
|
|
|
|
pub quantity: i64,
|
|
|
|
|
/// Of those, copies physically in the deck.
|
|
|
|
|
pub pulled: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Where a deck stands physically. Only building and built decks hold cards
|
|
|
|
|
/// and claim owned copies; ideas and retired decks are just lists.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum DeckStatus {
|
|
|
|
|
#[default]
|
|
|
|
|
Idea,
|
|
|
|
|
Building,
|
|
|
|
|
Built,
|
|
|
|
|
Retired,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// SQL list of the statuses for which [`DeckStatus::is_physical`] holds.
|
|
|
|
|
pub const PHYSICAL_STATUSES_SQL: &str = "('building', 'built')";
|
|
|
|
|
|
|
|
|
|
impl DeckStatus {
|
|
|
|
|
pub fn as_str(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
DeckStatus::Idea => "idea",
|
|
|
|
|
DeckStatus::Building => "building",
|
|
|
|
|
DeckStatus::Built => "built",
|
|
|
|
|
DeckStatus::Retired => "retired",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Unknown values (say, from a newer build) read as an idea, which holds
|
|
|
|
|
/// nothing — the safe direction to be wrong in.
|
|
|
|
|
pub fn parse(s: &str) -> Self {
|
|
|
|
|
match s {
|
|
|
|
|
"building" => DeckStatus::Building,
|
|
|
|
|
"built" => DeckStatus::Built,
|
|
|
|
|
"retired" => DeckStatus::Retired,
|
|
|
|
|
_ => DeckStatus::Idea,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_physical(self) -> bool {
|
|
|
|
|
matches!(self, DeckStatus::Building | DeckStatus::Built)
|
|
|
|
|
}
|
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.
2026-10-16 21:06:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One printing of a card, for the printing picker and copy labels.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct PrintingRef {
|
|
|
|
|
pub scryfall_id: String,
|
|
|
|
|
/// Scryfall's set code, lowercase (e.g. "2x2").
|
|
|
|
|
pub set_code: String,
|
|
|
|
|
pub collector_number: String,
|
|
|
|
|
/// Language code ("en", "ja", ...); empty on an older reference.db.
|
|
|
|
|
pub lang: String,
|
|
|
|
|
/// The finishes this printing exists in: "nonfoil", "foil", "etched".
|
|
|
|
|
pub finishes: Vec<String>,
|
|
|
|
|
pub rarity: String,
|
|
|
|
|
pub image_uri: String,
|
|
|
|
|
pub back_image_uri: String,
|
|
|
|
|
/// Scryfall's `prices.usd` for this printing, 0 if unknown.
|
|
|
|
|
pub price_usd: f64,
|
|
|
|
|
/// Release date as YYYY-MM-DD; empty on an older reference.db.
|
|
|
|
|
pub released_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A group of identical owned copies pinned to one printing + finish. Groups
|
|
|
|
|
/// are a subset of the card's `quantity_owned`; copies not in any group are
|
|
|
|
|
/// simply unspecified.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct LibraryCopy {
|
|
|
|
|
pub scryfall_id: String,
|
|
|
|
|
pub finish: String,
|
|
|
|
|
pub quantity: i64,
|
|
|
|
|
/// The printing's details, or `None` when the id no longer resolves in
|
|
|
|
|
/// the reference db (e.g. after a re-ingest dropped it).
|
|
|
|
|
pub printing: Option<PrintingRef>,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LibraryCard {
|
|
|
|
|
pub fn is_over_assigned(&self) -> bool {
|
|
|
|
|
self.quantity_assigned > self.quantity_owned
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A deck.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Deck {
|
|
|
|
|
pub id: i64,
|
|
|
|
|
pub name: String,
|
|
|
|
|
/// Free-text format, e.g. "Commander", "Modern" (empty if unset).
|
|
|
|
|
pub format: String,
|
|
|
|
|
/// The commander card's oracle id (meaningful for Commander/Brawl formats).
|
|
|
|
|
pub commander_oracle_id: Option<String>,
|
|
|
|
|
pub notes: String,
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
/// The card whose art headlines the deck in the deck list, if chosen;
|
|
|
|
|
/// `None` leaves the pick to the UI.
|
|
|
|
|
pub cover_oracle_id: Option<String>,
|
2026-10-26 22:35:41 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub status: DeckStatus,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-07 21:21:33 -05:00
|
|
|
/// Aggregate stats about the owned library, for a dashboard-style summary.
|
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct LibraryStats {
|
|
|
|
|
/// Distinct oracle cards owned (quantity_owned > 0).
|
|
|
|
|
pub unique_cards: i64,
|
|
|
|
|
/// Total physical copies owned, summed across all cards.
|
|
|
|
|
pub total_copies: i64,
|
|
|
|
|
pub deck_count: i64,
|
|
|
|
|
/// Copies owned in each color, WUBRG order. A multicolor card's copies
|
|
|
|
|
/// count toward every color it contains, so these can sum to more than
|
|
|
|
|
/// `total_copies`.
|
|
|
|
|
pub color_counts: [i64; 5],
|
|
|
|
|
/// Copies owned with no color (colors == 0).
|
|
|
|
|
pub colorless_count: i64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
/// Distinct type/subtype/keyword values found among *owned* cards, sorted.
|
|
|
|
|
/// Powers autocomplete for the library filter panel — e.g. no point
|
|
|
|
|
/// suggesting "Phyrexian" as a subtype if you don't own any.
|
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct LibraryFacets {
|
|
|
|
|
pub types: Vec<String>,
|
|
|
|
|
pub subtypes: Vec<String>,
|
|
|
|
|
pub keywords: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
/// Criteria for searching the owned library. All fields are optional; an empty
|
|
|
|
|
/// filter matches every owned card.
|
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub struct LibraryFilter {
|
|
|
|
|
/// Fuzzy-ish name text (applied as a case-insensitive substring here; the
|
|
|
|
|
/// fast fuzzy path is [`crate::search`]).
|
|
|
|
|
pub text: Option<String>,
|
|
|
|
|
/// Cards whose `colors` exactly equal this set.
|
|
|
|
|
pub colors_exact: Option<ColorSet>,
|
|
|
|
|
/// Cards whose `colors` include at least these.
|
|
|
|
|
pub colors_include: Option<ColorSet>,
|
|
|
|
|
/// Commander compatibility: only cards whose color identity is a subset of
|
|
|
|
|
/// this set (playable under a commander of this identity).
|
|
|
|
|
pub commander_identity: Option<ColorSet>,
|
|
|
|
|
pub cmc_min: Option<f64>,
|
|
|
|
|
pub cmc_max: Option<f64>,
|
|
|
|
|
/// Card supertype/type, e.g. "Creature", "Instant" (matched against the
|
|
|
|
|
/// normalized `card_types` table).
|
|
|
|
|
pub types: Vec<String>,
|
|
|
|
|
/// Subtype, e.g. "Elf", "Equipment".
|
|
|
|
|
pub subtypes: Vec<String>,
|
|
|
|
|
/// Keyword ability, e.g. "Flying", "Deathtouch".
|
|
|
|
|
pub keywords: Vec<String>,
|
|
|
|
|
/// Restrict to cards assigned to this deck id.
|
|
|
|
|
pub in_deck: Option<i64>,
|
2026-07-07 21:34:58 -05:00
|
|
|
/// Case-insensitive substring match against the card's stored location
|
|
|
|
|
/// (e.g. "Box 1"). Cards with no location set never match a non-empty value.
|
|
|
|
|
pub location: Option<String>,
|
2026-07-08 23:54:47 -05:00
|
|
|
/// Case-insensitive substring match against the card's oracle rules text.
|
|
|
|
|
pub oracle_text: Option<String>,
|
Rework library sync around a JSON document; add card filters
Sync
The previous design put the merge inside SQL: timestamp-stamping triggers,
a tombstone table, per-device SQLite snapshots, a device id in the database,
and row merges written as correlated-subquery UPDATEs. All of that schema
was inherited by desktop, and the merge was very hard to verify.
Replace it with a single JSON document plus a three-way merge in plain Rust.
Storing the last-read remote document as the merge base makes "who changed
what" and "what was deleted" derivable, which removes the tombstone table,
the updated_at columns and all eight triggers. It also makes a single shared
file safe: if a publish is lost the remote is unchanged, so the next merge
re-detects the local edits and republishes rather than reading their absence
as a deletion. That removes the need for per-device files and for device
identity entirely. Schema impact drops to one column, decks.uuid.
Conflicts resolve to "theirs", which is required for convergence rather than
chosen for taste — preferring ours has each device re-asserting its own value
forever. Every such choice is reported to the user.
SQLite becomes invisible: the working copy lives in the app data dir and the
user only ever names a .json file. There is always a document, defaulting to
private storage, so edits are never stranded with nowhere to be saved.
Pointing at a file that doesn't exist yet moves the current library there;
pointing at one that does adopts it. A converter brings a pre-document
library.db across.
Android reaches the folder through the Storage Access Framework rather than
MANAGE_EXTERNAL_STORAGE, so no storage permission is declared at all.
Filters
Track card origin in reference.db: Universes Beyond, digital-only and
Un-set cards, plus numeric power/toughness. Each origin flag is derived as
"every printing", not "any printing" — ordinary cards get reprinted into
those sets, and Lightning Bolt appearing in Marvel Commander must not hide
it. Non-numeric power ("*", "1+*") stays NULL so it drops out of range
filters instead of being compared as zero.
Also wire up filters over data already present: price range, format legality
(restricted counts as legal) and "can be a commander". All of these degrade
silently against an older reference.db rather than failing every query.
The shared criteria live in one component used by both filter panels, as a
compact row of icon toggles.
Card Search gains the Library tab's bulk selection: checkboxes in both views,
select-all, and a bulk add-to-deck with a single undo for the whole batch.
2026-08-07 22:03:49 -05:00
|
|
|
/// Hide cards that exist only as Universes Beyond (Lord of the Rings,
|
|
|
|
|
/// Warhammer 40,000, Marvel, ...). Ordinary cards reprinted into a UB set
|
|
|
|
|
/// are kept — see `oracle_cards.universes_beyond`.
|
|
|
|
|
///
|
|
|
|
|
/// Silently inert against a `reference.db` built before that column
|
|
|
|
|
/// existed, the same way the other reference-schema additions degrade.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub exclude_universes_beyond: bool,
|
|
|
|
|
/// Hide cards that exist only on Arena/MTGO (Alchemy rebalances and the
|
|
|
|
|
/// like) and so can never be in a physical collection.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub exclude_digital_only: bool,
|
|
|
|
|
/// Hide cards that exist only in "funny" sets (Unglued, Unhinged, Unstable,
|
|
|
|
|
/// Unfinity).
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub exclude_funny: bool,
|
|
|
|
|
/// Cheapest known printing, in USD. Cards with no known price count as 0,
|
|
|
|
|
/// matching how they're displayed.
|
|
|
|
|
pub price_min: Option<f64>,
|
|
|
|
|
pub price_max: Option<f64>,
|
|
|
|
|
/// Only cards legal in this format (a [`LEGALITY_FORMATS`] slug, e.g.
|
|
|
|
|
/// "commander"). Excludes banned and not-legal; restricted still counts as
|
|
|
|
|
/// legal, since a restricted card is playable as a single copy.
|
|
|
|
|
pub legal_in: Option<String>,
|
|
|
|
|
/// Only cards that can head a Commander deck.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub can_be_commander: bool,
|
2026-08-14 01:02:29 -05:00
|
|
|
/// Only "vanilla" creatures — a creature with an empty rules box. Includes
|
|
|
|
|
/// Adventure and Prepared cards whose creature half has no abilities, which
|
|
|
|
|
/// is why this reads a precomputed column instead of testing `oracle_text`:
|
|
|
|
|
/// that column joins every face, so the attached spell's text is in it.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub vanilla_only: bool,
|
Add colorless filters, and make set listings filterable
Colorless
There was no way to ask for colorless cards — the colour toggles are a 5-bit
WUBRG mask where 0 already means "don't filter by colour", leaving nothing to
express "no colours at all". That's exactly what you need to find a colorless
commander's card pool.
Adds a "C" pip to both colour rows, matching how ColorPips already renders
colorless cards, backed by two explicit booleans rather than a sixth bit — a
filter mask and a card's real colour mask staying the same shape is worth
keeping. The two rows mean different things and both are useful: on the
commander row C filters colour *identity*, which is the Commander rule; on the
includes row it filters the card's own colours. A card printed colorless can
still carry a coloured identity through mana symbols in its text, and it's the
identity that bars it from a colorless commander's deck.
Set listings
News > Releases had its own card path and so couldn't be filtered. A new
set_code filter (matched against printings.set_code, already ingested) lets the
set browser run through search_reference instead, so every criterion works
there identically rather than being reimplemented over a live Scryfall
response.
That alone would have cost the ability to browse an upcoming set, since
search_reference only knows downloaded card data — and upcoming sets are half
the point of the tab. So the browser picks a source per set: downloaded data
where it exists, with the full filter panel; otherwise Scryfall directly, with
the panel hidden and a line explaining why. A panel that silently does nothing
would be worse than no panel.
The source is decided by an *unfiltered* probe. Keying off whether the current
criteria match anything would flip a properly indexed set to "live" the moment
a filter excluded everything, dropping the panel exactly when it was in use.
2026-08-10 22:34:46 -05:00
|
|
|
/// Only cards with an empty colour identity — what a colourless commander
|
|
|
|
|
/// can actually play. The colour bitmasks can't express this: 0 there means
|
|
|
|
|
/// "no filter", not "no colours".
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub colorless_identity: bool,
|
|
|
|
|
/// Only cards printed in this set (matched against `printings.set_code`).
|
|
|
|
|
/// Lets the release browser reuse the whole criteria pipeline instead of
|
|
|
|
|
/// reimplementing it over a live Scryfall response.
|
|
|
|
|
pub set_code: Option<String>,
|
|
|
|
|
/// Only cards that are themselves colourless (`colors` empty). Distinct
|
|
|
|
|
/// from `colorless_identity`: a card can be colourless yet carry a coloured
|
|
|
|
|
/// identity through mana symbols in its rules text.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub colorless_colors: bool,
|
Rework library sync around a JSON document; add card filters
Sync
The previous design put the merge inside SQL: timestamp-stamping triggers,
a tombstone table, per-device SQLite snapshots, a device id in the database,
and row merges written as correlated-subquery UPDATEs. All of that schema
was inherited by desktop, and the merge was very hard to verify.
Replace it with a single JSON document plus a three-way merge in plain Rust.
Storing the last-read remote document as the merge base makes "who changed
what" and "what was deleted" derivable, which removes the tombstone table,
the updated_at columns and all eight triggers. It also makes a single shared
file safe: if a publish is lost the remote is unchanged, so the next merge
re-detects the local edits and republishes rather than reading their absence
as a deletion. That removes the need for per-device files and for device
identity entirely. Schema impact drops to one column, decks.uuid.
Conflicts resolve to "theirs", which is required for convergence rather than
chosen for taste — preferring ours has each device re-asserting its own value
forever. Every such choice is reported to the user.
SQLite becomes invisible: the working copy lives in the app data dir and the
user only ever names a .json file. There is always a document, defaulting to
private storage, so edits are never stranded with nowhere to be saved.
Pointing at a file that doesn't exist yet moves the current library there;
pointing at one that does adopts it. A converter brings a pre-document
library.db across.
Android reaches the folder through the Storage Access Framework rather than
MANAGE_EXTERNAL_STORAGE, so no storage permission is declared at all.
Filters
Track card origin in reference.db: Universes Beyond, digital-only and
Un-set cards, plus numeric power/toughness. Each origin flag is derived as
"every printing", not "any printing" — ordinary cards get reprinted into
those sets, and Lightning Bolt appearing in Marvel Commander must not hide
it. Non-numeric power ("*", "1+*") stays NULL so it drops out of range
filters instead of being compared as zero.
Also wire up filters over data already present: price range, format legality
(restricted counts as legal) and "can be a commander". All of these degrade
silently against an older reference.db rather than failing every query.
The shared criteria live in one component used by both filter panels, as a
compact row of icon toggles.
Card Search gains the Library tab's bulk selection: checkboxes in both views,
select-all, and a bulk add-to-deck with a single undo for the whole batch.
2026-08-07 22:03:49 -05:00
|
|
|
/// Printed power/toughness range. Cards whose printed value isn't a plain
|
|
|
|
|
/// number ("*", "1+*") are excluded from these, since they can't be
|
|
|
|
|
/// compared — see `oracle_cards.power`.
|
|
|
|
|
pub power_min: Option<f64>,
|
|
|
|
|
pub power_max: Option<f64>,
|
|
|
|
|
pub toughness_min: Option<f64>,
|
|
|
|
|
pub toughness_max: Option<f64>,
|
2026-08-22 19:14:27 -05:00
|
|
|
/// Only cards with zero owned copies. Meaningless against
|
|
|
|
|
/// [`Catalog::search_library`] (which already only returns owned cards),
|
|
|
|
|
/// so it's only honored by [`Catalog::search_reference`] — the Card
|
|
|
|
|
/// Search tab's "Not owned" quick filter.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub not_owned: bool,
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
/// Only cards with copies to spare: more owned than the decks' main
|
|
|
|
|
/// boards use between them (the opposite of the over-assigned ⚠). A card
|
|
|
|
|
/// in no deck at all has every copy spare. Maybeboards don't count, the
|
|
|
|
|
/// same as for over-assignment.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub spares_only: bool,
|
|
|
|
|
/// Only cards printed at one of these rarities ("common", "uncommon",
|
|
|
|
|
/// "rare", "mythic"); any one of them matches. The library tracks cards
|
|
|
|
|
/// rather than printings, so any printing counts — narrowed to the
|
|
|
|
|
/// printing in [`Self::set_code`] when that's set.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub rarities: Vec<String>,
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
}
|
2026-07-20 00:21:22 -05:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn legalities_round_trip_all_statuses() {
|
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
|
map.insert("standard".to_string(), "legal".to_string());
|
|
|
|
|
map.insert("modern".to_string(), "banned".to_string());
|
|
|
|
|
map.insert("vintage".to_string(), "restricted".to_string());
|
|
|
|
|
// "pauper" deliberately absent
|
|
|
|
|
|
|
|
|
|
let legalities = Legalities::from_scryfall(&map);
|
|
|
|
|
|
|
|
|
|
assert_eq!(legalities.status_for("Standard"), LegalStatus::Legal);
|
|
|
|
|
assert_eq!(legalities.status_for("Modern"), LegalStatus::Banned);
|
|
|
|
|
assert_eq!(legalities.status_for("Vintage"), LegalStatus::Restricted);
|
|
|
|
|
assert_eq!(legalities.status_for("Pauper"), LegalStatus::NotLegal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn legalities_unknown_format_label_is_not_legal() {
|
|
|
|
|
let legalities = Legalities::from_scryfall(&HashMap::new());
|
|
|
|
|
assert_eq!(legalities.status_for("Nonsense"), LegalStatus::NotLegal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn legalities_default_is_all_not_legal() {
|
|
|
|
|
let legalities = Legalities::default();
|
|
|
|
|
assert_eq!(legalities.status_for("Commander"), LegalStatus::NotLegal);
|
|
|
|
|
assert_eq!(legalities.status_for("Brawl"), LegalStatus::NotLegal);
|
|
|
|
|
}
|
|
|
|
|
}
|