// Typed wrappers over the Tauri command bridge into mtgc-core. import { invoke } from "@tauri-apps/api/core"; // --- Types (mirror the serde-serialized core structs) --------------------- export interface CardRef { oracle_id: string; name: string; mana_value: number; colors: number; // WUBRG bitmask color_identity: number; // WUBRG bitmask type_line: string; // Printed/displayed sideways (Battles, Planes, Phenomena). The stored // image itself is portrait-shaped with the content rotated, so viewers // must rotate it 90° to display right-side up. horizontal: boolean; image_uri: string; // Scryfall "normal" image URL, or "" if unavailable back_image_uri: string; // back face for transform/MDFC/reversible cards, else "" produced_mana: number; // WUBRG bitmask of mana this card can tap for produces_colorless: boolean; // can produce {C} price_usd: number; // USD market price, or 0 if unknown legalities: number; // 2 bits/format bitmask, see LEGALITY_FORMATS } export interface LibraryCard extends CardRef { quantity_owned: number; decks: string[]; quantity_assigned: number; location: string; } export interface DeckEntry extends CardRef { quantity: number; is_commander: boolean; /** On the maybeboard: tracked against the deck but excluded from deck * analysis stats and library assignment totals. */ is_maybeboard: boolean; quantity_owned: number; } /** * A `CardRef` plus whatever ownership context the caller happens to have * (present on `LibraryCard`/`DeckEntry`, absent on a bare `CardRef`). The * click-to-preview modal's info tile renders these fields when present and * omits them otherwise — callers don't need to do anything special, since * `LibraryCard`/`DeckEntry`/`CardRef` are all structurally assignable here. */ export type PreviewCard = CardRef & { quantity_owned?: number; decks?: string[]; }; export interface Deck { id: number; name: string; format: string; commander_oracle_id: string | null; notes: string; } export interface DeckImportResult { deck: Deck; unresolved: string[]; } export interface LibraryImportResult { added: [string, number][]; unresolved: string[]; } export interface SynergyReason { category: string; // "Tribal" | "Mechanic" | "Archetype" | "Mana base" /** The specific thing matched — "Dragon", "Flying", "Landfall". Separate from * `detail` so the UI can filter on it: a deck usually has several archetypes * and you want one of them, not all. */ subject: string; detail: string; } export interface CardSuggestion { card: CardRef; quantity_owned: number; score: number; reasons: SynergyReason[]; edhrec_rank: number | null; } export interface LibraryFilter { text?: string | null; colors_exact?: number | null; colors_include?: number | null; commander_identity?: number | null; cmc_min?: number | null; cmc_max?: number | null; types?: string[]; subtypes?: string[]; keywords?: string[]; in_deck?: number | null; location?: string | null; oracle_text?: string | null; /** 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. Needs card data downloaded after this shipped; inert otherwise. */ exclude_universes_beyond?: boolean; /** Hide Arena/MTGO-only cards (Alchemy rebalances) — they can't be owned on * paper. Needs card data downloaded after this shipped; inert otherwise. */ exclude_digital_only?: boolean; /** Hide Un-set cards (Unglued, Unhinged, Unstable, Unfinity). */ exclude_funny?: boolean; /** Cheapest known printing in USD; cards with no known price count as 0. */ price_min?: number | null; price_max?: number | null; /** Format slug ("commander", "modern", …). Legal and restricted match. */ legal_in?: string | null; /** Only cards that can head a Commander deck. */ can_be_commander?: boolean; /** Only cards printed in this set (matched against printings.set_code). * Lets the release browser reuse the whole criteria pipeline. */ set_code?: string | null; /** Only cards with an empty color identity — a colorless commander's pool. * Separate from the color bitmasks, where 0 means "no filter". */ colorless_identity?: boolean; /** Only cards that are themselves colorless. */ colorless_colors?: boolean; /** Printed power/toughness. Cards printed with "*" or "1+*" are excluded, * since those can't be compared numerically. */ power_min?: number | null; power_max?: number | null; toughness_min?: number | null; toughness_max?: number | null; } export interface Status { library_path: string; reference_path: string; reference_ready: boolean; card_count: number; reference_updated_at: number | null; // unix seconds /** True on mobile builds. Used to hide affordances that depend on choosing an * arbitrary filesystem path for library.db — Android scoped storage has no * equivalent, and SQLite can't be backed by a content:// URI. */ is_mobile: boolean; } /** State of the user's library document — the JSON file they keep in whatever * folder they sync. The SQLite database behind it is internal and never shown. */ export interface SyncStatus { /** Whether a document has been chosen. */ chosen: boolean; /** Whether it's currently reachable. Distinct from `chosen`: an Android grant * can be revoked and a desktop folder can be deleted or unmounted — both need * re-picking rather than first-time setup. */ granted: boolean; label: string | null; /** True where the folder is chosen through the system picker (Android) rather * than by path — the one thing the UI branches on. */ picker_based: boolean; /** A pre-document library.db from an older build, waiting to be converted. */ legacy_library: string | null; /** Which file in the granted folder is the library (Android). Null on * desktop, where the whole path is chosen at once. */ document_name: string | null; } /** What one sync did. */ export interface SyncReport { /** Whether the merge altered the local library. */ changed: boolean; /** Whether the document was rewritten (skipped when we had nothing to add). */ published: boolean; /** Conflict copies absorbed and cleaned up. */ absorbed: number; report: { cards_changed: number; decks_changed: number; decks_removed: number; /** Fields both sides changed differently, described in user terms. */ conflicts: string[]; }; } /** A published or upcoming Magic set, from Scryfall. */ export interface SetInfo { code: string; name: string; set_type: string; /** ISO date; null for sets with no announced date, which sort last. */ released_at: string | null; card_count: number; icon_uri: string; scryfall_uri: string; } /** A news article — title and link only; the app links out rather than * reproducing anyone's writing. */ export interface Article { category: string; title: string; url: string; } export interface News { sets: SetInfo[]; articles: Article[]; /** Unix seconds of the last successful fetch; 0 if never. */ fetched_at: number; /** False when the article source couldn't be reached or its markup changed. * Only that section is affected — releases stay usable. */ articles_ok: boolean; } /** A card in a set. Extends CardRef so it renders with the same CardTile — * and therefore the same click-to-preview — as every other card in the app. */ export interface SetCard extends CardRef { collector_number: string; quantity_owned: number; } export interface LibraryStats { unique_cards: number; total_copies: number; deck_count: number; color_counts: [number, number, number, number, number]; // W, U, B, R, G colorless_count: number; } export interface LibraryFacets { types: string[]; subtypes: string[]; keywords: string[]; } export interface IngestStageInfo { id: string; label: string; weight: number; // fraction of total work, 0..1, sums to 1 across all stages } export interface IngestProgress { stage: "index" | "oracle" | "printings" | "optimize" | string; message: string; overall_percent: number; // 0..100 cumulative across all stages stages: IngestStageInfo[]; } // --- WUBRG bitmask helpers ------------------------------------------------ export const COLORS: { bit: number; code: string; label: string }[] = [ { bit: 1, code: "W", label: "White" }, { bit: 2, code: "U", label: "Blue" }, { bit: 4, code: "B", label: "Black" }, { bit: 8, code: "R", label: "Red" }, { bit: 16, code: "G", label: "Green" }, ]; export function maskToLetters(mask: number): string { return COLORS.filter((c) => mask & c.bit) .map((c) => c.code) .join(""); } // --- Legality bitmask helpers ---------------------------------------------- // Mirrors mtgc-core's model::LEGALITY_FORMATS / Legalities exactly: 2 bits per // format, in this fixed order. Do not reorder. export const LEGALITY_FORMATS: { key: string; label: string }[] = [ { key: "standard", label: "Standard" }, { key: "pioneer", label: "Pioneer" }, { key: "modern", label: "Modern" }, { key: "legacy", label: "Legacy" }, { key: "vintage", label: "Vintage" }, { key: "pauper", label: "Pauper" }, { key: "commander", label: "Commander" }, { key: "brawl", label: "Brawl" }, ]; export type LegalStatus = "not_legal" | "legal" | "banned" | "restricted"; const LEGAL_STATUS_CODES: readonly LegalStatus[] = ["not_legal", "legal", "banned", "restricted"]; export function legalStatusFor(legalities: number, formatLabel: string): LegalStatus { const i = LEGALITY_FORMATS.findIndex((f) => f.label === formatLabel); if (i < 0) return "not_legal"; const code = (legalities >> (i * 2)) & 0b11; return LEGAL_STATUS_CODES[code]; } // --- Commands ------------------------------------------------------------- export const api = { getStatus: () => invoke("get_status"), updateCardData: () => invoke("update_card_data"), cancelUpdateCardData: () => invoke("cancel_update_card_data"), /** Adopt an already-built reference.db from `path` instead of downloading it. */ importReferenceDb: (path: string) => invoke("import_reference_db", { path }), syncStatus: () => invoke("sync_status"), /** Merge the library document with local edits and publish the result. * Resolves with `null` when a sync was already running and this call stood * down — expected, since sync also fires automatically. */ syncNow: () => invoke("sync_now"), /** Android: grant access to a folder through the system picker. */ syncPickFolder: () => invoke("sync_pick_folder"), /** Desktop: adopt a library document chosen with the file dialog. */ syncSetDocument: (path: string) => invoke("sync_set_document", { path }), syncForgetDocument: () => invoke("sync_forget_document"), /** Library files already in the chosen folder, for Android's second step. */ syncListDocuments: () => invoke("sync_list_documents"), /** Android: choose which file in the granted folder is the library. */ syncSetDocumentName: (name: string) => invoke("sync_set_document_name", { name }), /** Convert a pre-document library.db into the current format. */ convertLegacyLibrary: (path: string) => invoke("convert_legacy_library", { path }), /** Set releases and article links. `refresh` forces a fetch past the cache. */ getNews: (refresh = false) => invoke("get_news", { refresh }), /** Every card in a set, annotated with how many you own. */ getSetCards: (code: string) => invoke("get_set_cards", { code }), searchCards: (query: string, limit = 20) => invoke("search_cards", { query, limit }), addOwned: (oracleId: string, delta: number) => invoke("add_owned", { oracleId, delta }), setOwned: (oracleId: string, quantity: number) => invoke("set_owned", { oracleId, quantity }), setLocation: (oracleId: string, location: string) => invoke("set_location", { oracleId, location }), searchLibrary: (filter: LibraryFilter) => invoke("search_library", { filter }), searchReference: (filter: LibraryFilter, limit: number) => invoke("search_reference", { filter, limit }), getLibraryStats: () => invoke("get_library_stats"), getLibraryFacets: () => invoke("get_library_facets"), getReferenceFacets: () => invoke("get_reference_facets"), listDecks: () => invoke("list_decks"), createDeck: (name: string, commanderOracleId: string | null) => invoke("create_deck", { name, commanderOracleId }), deleteDeck: (deckId: number) => invoke("delete_deck", { deckId }), renameDeck: (deckId: number, name: string) => invoke("rename_deck", { deckId, name }), duplicateDeck: (deckId: number, newName: string) => invoke("duplicate_deck", { deckId, newName }), setDeckFormat: (deckId: number, format: string) => invoke("set_deck_format", { deckId, format }), setDeckCommander: (deckId: number, oracleId: string | null) => invoke("set_deck_commander", { deckId, oracleId }), setDeckCard: ( deckId: number, oracleId: string, quantity: number, isCommander = false, isMaybeboard = false, ) => invoke("set_deck_card", { deckId, oracleId, quantity, isCommander, isMaybeboard }), deckContents: (deckId: number) => invoke("deck_contents", { deckId }), suggestCards: (deckId: number, offset = 0, unusual = false) => invoke("suggest_cards", { deckId, offset, unusual }), cardDecks: (oracleId: string) => invoke("card_decks", { oracleId }), exportDeck: (deckId: number, path: string) => invoke("export_deck", { deckId, path }), importDeck: (path: string, name: string) => invoke("import_deck", { path, name }), exportLibraryCards: (oracleIds: string[], path: string) => invoke("export_library_cards", { oracleIds, path }), importLibraryCards: (path: string) => invoke("import_library_cards", { path }), };