// 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" 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; } 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; } 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"), setLibraryPath: (path: string) => invoke("set_library_path", { path }), 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 }), 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 }), };