// 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; /** The printing whose art this slot shows; `image_uri` already reflects it. */ preferred_scryfall_id: string | null; } /** One printing of a card, for the printing picker and copy labels. */ export interface PrintingRef { scryfall_id: string; set_code: string; // lowercase, e.g. "2x2" collector_number: string; lang: string; // "" on an older card database finishes: string[]; // "nonfoil" | "foil" | "etched" rarity: string; image_uri: string; back_image_uri: string; price_usd: number; released_at: string; // YYYY-MM-DD, "" on an older card database } /** Owned copies pinned to one printing + finish. A subset of the card's * `quantity_owned`; copies in no group are unspecified. */ export interface LibraryCopy { scryfall_id: string; finish: string; quantity: number; /** Null when the printing has left the card database. */ printing: PrintingRef | null; } /** * 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[]; preferred_scryfall_id?: string | null; }; /** * Where a click preview was opened from, when the caller lets it edit * printings: the info tile then tracks owned copies by printing, and — for a * deck slot — chooses which printing's art the slot shows. */ export interface PreviewContext { /** Called after any printing change, so the caller can refetch. */ onChanged: () => void; /** The deck the previewed card is a slot of. */ deckId?: number; } 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; /** Copies other decks' main boards already use. */ quantity_assigned: 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 creatures with an empty rules box (Adventure/Prepared creature halves count). */ vanilla_only?: 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; /** Only cards with zero owned copies. Only honored by `searchReference` * (`searchLibrary` already returns owned cards exclusively). */ not_owned?: boolean; /** Only cards with more copies owned than the decks' main boards use. */ spares_only?: boolean; /** Only cards printed at any of these rarities ("common", "uncommon", * "rare", "mythic"). Any printing counts; with `set_code`, that set's. */ rarities?: string[]; } export interface Status { /** The open library's name and where it syncs: "Main · user @ host", or * this device only. Changes whenever another library is opened. */ library_label: string; /** The open library. */ library: { id: string; name: string }; reference_path: string; reference_ready: boolean; card_count: number; reference_updated_at: number | null; // unix seconds /** Columns the installed card data lacks ("table.column") because an older * version built it. Non-empty means some features are degraded until the * user updates card data. */ reference_missing_columns: string[]; /** 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; } /** The account a device is signed in to on a sync server. */ export interface SyncAccount { id: string; username: string; display_name: string; email: string; /** "local" (username/password) or "oidc" (single sign-on). */ method: string; } /** Library sync with a sync server. The library itself always lives on this * device and works offline; syncing exchanges edits with the other devices. */ export interface SyncStatus { signed_in: boolean; /** Kept after signing out, to offer signing back in. */ server_url: string | null; account: SyncAccount | null; /** Unix seconds of the last completed sync. */ last_synced_at: number | null; /** Why the last sync failed, until one succeeds. */ last_error: string | null; /** The first sync found libraries both here and on the server; waiting on * `syncConnect` to say whether to combine them or take the server's. */ needs_choice: boolean; /** Edits made here that the server hasn't acknowledged yet. */ pending_changes: boolean; /** A sign-in is waiting on the browser. */ signing_in: boolean; /** A pre-document library.db from an older build, waiting to be imported. */ legacy_library: string | null; } /** One of the account's signed-in devices. */ export interface SyncDevice { id: number; name: string; created_at: number; last_seen_at: number; /** Whether it's this device. */ current: boolean; } export type FirstSync = "combine" | "use_server"; export interface MergeReport { cards_changed: number; decks_changed: number; decks_removed: number; /** Fields both sides changed, where the server's value was kept. */ conflicts: string[]; } /** What an import of a library file brought in. */ export interface ImportReport { cards: number; decks: number; report: MergeReport; } /** What one sync did. */ export interface SyncReport { /** Whether the merge altered this device's library. */ changed: boolean; /** Whether the server's copy was replaced (skipped when there was nothing to add). */ published: boolean; /** The first sync is waiting on the user — see `SyncStatus.needs_choice`. */ needs_choice: boolean; report: MergeReport; /** Libraries were added, renamed, or deleted on the server: re-list them. */ libraries_changed: boolean; } /** One of the libraries on this device. */ export interface LibraryInfo { /** Local only; never shown. */ id: string; name: string; /** The one that's open. */ active: boolean; /** The library the device started with, which can't be deleted. */ is_default: boolean; /** Whether it should be on the sync server. */ sync: boolean; /** Whether it's linked to a library on the server yet. */ linked: boolean; /** Another device deleted it from the server; this copy is all that's left. */ server_deleted: boolean; } /** The open library, for `libraryKey`. Set by `App` from each status. */ let openLibraryId = "default"; export function setOpenLibrary(id: string) { openLibraryId = id; } /** A localStorage key for something that belongs to the open library — a * selected deck, say — so each library remembers its own. The default * library keeps the bare key it always had. */ export function libraryKey(key: string): string { return openLibraryId === "default" ? key : `${key}:${openLibraryId}`; } /** 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"), /** Sign in through the browser, then sync. Resolves once the user finishes * (or abandons) signing in there. */ syncSignIn: (serverUrl: string, deviceName?: string) => invoke("sync_sign_in", { serverUrl, deviceName: deviceName || null }), syncCancelSignIn: () => invoke("sync_cancel_sign_in"), /** Sign this device out. The library stays here. */ syncSignOut: () => invoke("sync_sign_out"), /** Merge with the server and publish local edits. Resolves with `null` when * not signed in, or a sync was already running and this call stood down — * expected, since sync also fires automatically. */ syncNow: () => invoke("sync_now"), /** Answer the first-sync question. */ syncConnect: (choice: FirstSync) => invoke("sync_connect", { choice }), syncDevices: () => invoke("sync_devices"), /** Sign another device out (a lost phone, say); returns the remaining list. */ syncRevokeDevice: (id: number) => invoke("sync_revoke_device", { id }), listLibraries: () => invoke("list_libraries"), /** Start a new, empty library and open it. Resolves with the new list. */ createLibrary: (name: string, sync: boolean) => invoke("create_library", { name, sync }), /** Open another library. */ switchLibrary: (id: string) => invoke("switch_library", { id }), /** Rename a library, on the sync server too when it's there. */ renameLibrary: (id: string, name: string) => invoke("rename_library", { id, name }), /** Delete a library from this device; with `everywhere`, from the server too. */ deleteLibrary: (id: string, everywhere: boolean) => invoke("delete_library", { id, everywhere }), /** Put a library on the sync server, or keep it on this device only. */ setLibrarySync: (id: string, sync: boolean) => invoke("set_library_sync", { id, sync }), /** Merge a library file into the open one: a JSON backup, or an old library.db. */ importLibraryFile: (path: string) => invoke("import_library_file", { path }), /** Save the whole open library as JSON, as a backup. */ exportLibraryDocument: (path: string) => invoke("export_library_document", { 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, options: { unusual?: boolean; owned_only?: boolean } = {}) => invoke("suggest_cards", { deckId, offset, options }), cardDecks: (oracleId: string) => invoke("card_decks", { oracleId }), /** Every paper printing of a card, newest first. */ listPrintings: (oracleId: string) => invoke("list_printings", { oracleId }), listLibraryCopies: (oracleId: string) => invoke("list_library_copies", { oracleId }), /** Set how many owned copies are this printing + finish (0 removes the * group). Resolves with the card's owned quantity, which grows to fit. */ setLibraryCopy: (oracleId: string, scryfallId: string, finish: string, quantity: number) => invoke("set_library_copy", { oracleId, scryfallId, finish, quantity }), /** Choose which printing a deck slot shows; null restores the default. * Resolves with the slot afterwards (its art reflecting the choice). */ setDeckCardPrinting: (deckId: number, oracleId: string, scryfallId: string | null) => invoke("set_deck_card_printing", { deckId, oracleId, scryfallId }), 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 }), /** `location`, unless blank, is set on every imported card. */ importLibraryCards: (path: string, location: string) => invoke("import_library_cards", { path, location }), };