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
|
|
|
// 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;
|
2026-07-22 20:55:17 -05:00
|
|
|
// 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;
|
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
|
|
|
image_uri: string; // Scryfall "normal" image URL, or "" if unavailable
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
back_image_uri: string; // back face for transform/MDFC/reversible cards, else ""
|
2026-07-08 23:54:47 -05:00
|
|
|
produced_mana: number; // WUBRG bitmask of mana this card can tap for
|
|
|
|
|
produces_colorless: boolean; // can produce {C}
|
2026-07-14 21:32:25 -05:00
|
|
|
price_usd: number; // USD market price, or 0 if unknown
|
2026-07-20 00:21:22 -05:00
|
|
|
legalities: number; // 2 bits/format bitmask, see LEGALITY_FORMATS
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-07-14 21:32:25 -05:00
|
|
|
/** On the maybeboard: tracked against the deck but excluded from deck
|
|
|
|
|
* analysis stats and library assignment totals. */
|
|
|
|
|
is_maybeboard: boolean;
|
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
|
|
|
quantity_owned: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 23:11:26 -05:00
|
|
|
/**
|
|
|
|
|
* 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[];
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
export interface Deck {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
format: string;
|
|
|
|
|
commander_oracle_id: string | null;
|
|
|
|
|
notes: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
export interface DeckImportResult {
|
|
|
|
|
deck: Deck;
|
|
|
|
|
unresolved: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LibraryImportResult {
|
|
|
|
|
added: [string, number][];
|
|
|
|
|
unresolved: string[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 21:04:36 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
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;
|
2026-07-07 21:35:09 -05:00
|
|
|
location?: string | null;
|
2026-07-08 23:54:47 -05:00
|
|
|
oracle_text?: string | null;
|
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. 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;
|
|
|
|
|
/** 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;
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Status {
|
|
|
|
|
library_path: string;
|
|
|
|
|
reference_path: string;
|
|
|
|
|
reference_ready: boolean;
|
|
|
|
|
card_count: number;
|
2026-07-07 21:35:09 -05:00
|
|
|
reference_updated_at: number | null; // unix seconds
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
/** 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;
|
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
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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[];
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Add a News tab: set releases and links to Wizards' articles
The app knew nothing about the game outside your own collection — no way to
see that a set drops next week, or that a Secret Lair was announced.
Two sources with very different reliability, kept apart so one failing can't
take the page down.
Releases come from Scryfall's documented /sets endpoint. Clicking a set fetches
its cards live rather than reading reference.db, so a set released or spoiled
since the last card-data download still works; ownership is joined locally by
oracle_id, and the same add-to-library stepper works there as anywhere else.
Articles have no supported source at all: Wizards publishes no RSS or Atom feed
and no public API, and their page is a Nuxt app whose CSS classes are per-build
hashes. So the app links out rather than reproducing anyone's writing, which
keeps the scraped surface to titles and hrefs. parse_articles keys on nothing
cosmetic — anchors pointing at /en/news/<category>/<slug>, taking the anchor's
own text as the title. A restyle can't break that; only a change to their URL
scheme could, and the fixture tests fail loudly when it does rather than the
page quietly going empty. Verified against a live capture: 15 articles, correct
titles and categories.
Two things the fixtures caught. Where an article is linked several times per
card, the first link is a "Learn More" button, so the longest text wins rather
than the first. And a *successful* fetch yielding zero articles is treated as a
failure, since that is the signature of their markup changing.
get_set_cards returns a full CardRef rather than the handful of fields the grid
needs, so the tab renders the same CardTile as the rest of the app and gets
click-to-preview instead of a lookalike that does nothing when clicked.
Both feeds cache to news-cache.json for six hours and fall back to whatever was
last seen, so opening the tab offline shows stale content with its age rather
than an error.
Also promotes the HTTP agent out of ingest.rs into a shared http module, so
timeouts and User-Agent can't drift between the ingester and anything else that
reaches the network. Adds tauri-plugin-opener for opening articles externally.
2026-08-07 23:35:50 -05:00
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
export interface LibraryFacets {
|
|
|
|
|
types: string[];
|
|
|
|
|
subtypes: string[];
|
|
|
|
|
keywords: string[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
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[];
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// --- 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("");
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
// --- 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];
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// --- Commands -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export const api = {
|
|
|
|
|
getStatus: () => invoke<Status>("get_status"),
|
|
|
|
|
updateCardData: () => invoke<Status>("update_card_data"),
|
2026-07-07 20:22:31 -05:00
|
|
|
cancelUpdateCardData: () => invoke<void>("cancel_update_card_data"),
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
/** Adopt an already-built reference.db from `path` instead of downloading it. */
|
|
|
|
|
importReferenceDb: (path: string) => invoke<Status>("import_reference_db", { path }),
|
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
|
|
|
syncStatus: () => invoke<SyncStatus>("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<SyncReport | null>("sync_now"),
|
|
|
|
|
/** Android: grant access to a folder through the system picker. */
|
|
|
|
|
syncPickFolder: () => invoke<SyncStatus>("sync_pick_folder"),
|
|
|
|
|
/** Desktop: adopt a library document chosen with the file dialog. */
|
|
|
|
|
syncSetDocument: (path: string) => invoke<SyncStatus>("sync_set_document", { path }),
|
|
|
|
|
syncForgetDocument: () => invoke<SyncStatus>("sync_forget_document"),
|
|
|
|
|
/** Convert a pre-document library.db into the current format. */
|
|
|
|
|
convertLegacyLibrary: (path: string) => invoke<SyncReport>("convert_legacy_library", { path }),
|
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
|
|
|
|
Add a News tab: set releases and links to Wizards' articles
The app knew nothing about the game outside your own collection — no way to
see that a set drops next week, or that a Secret Lair was announced.
Two sources with very different reliability, kept apart so one failing can't
take the page down.
Releases come from Scryfall's documented /sets endpoint. Clicking a set fetches
its cards live rather than reading reference.db, so a set released or spoiled
since the last card-data download still works; ownership is joined locally by
oracle_id, and the same add-to-library stepper works there as anywhere else.
Articles have no supported source at all: Wizards publishes no RSS or Atom feed
and no public API, and their page is a Nuxt app whose CSS classes are per-build
hashes. So the app links out rather than reproducing anyone's writing, which
keeps the scraped surface to titles and hrefs. parse_articles keys on nothing
cosmetic — anchors pointing at /en/news/<category>/<slug>, taking the anchor's
own text as the title. A restyle can't break that; only a change to their URL
scheme could, and the fixture tests fail loudly when it does rather than the
page quietly going empty. Verified against a live capture: 15 articles, correct
titles and categories.
Two things the fixtures caught. Where an article is linked several times per
card, the first link is a "Learn More" button, so the longest text wins rather
than the first. And a *successful* fetch yielding zero articles is treated as a
failure, since that is the signature of their markup changing.
get_set_cards returns a full CardRef rather than the handful of fields the grid
needs, so the tab renders the same CardTile as the rest of the app and gets
click-to-preview instead of a lookalike that does nothing when clicked.
Both feeds cache to news-cache.json for six hours and fall back to whatever was
last seen, so opening the tab offline shows stale content with its age rather
than an error.
Also promotes the HTTP agent out of ingest.rs into a shared http module, so
timeouts and User-Agent can't drift between the ingester and anything else that
reaches the network. Adds tauri-plugin-opener for opening articles externally.
2026-08-07 23:35:50 -05:00
|
|
|
/** Set releases and article links. `refresh` forces a fetch past the cache. */
|
|
|
|
|
getNews: (refresh = false) => invoke<News>("get_news", { refresh }),
|
|
|
|
|
/** Every card in a set, annotated with how many you own. */
|
|
|
|
|
getSetCards: (code: string) => invoke<SetCard[]>("get_set_cards", { code }),
|
|
|
|
|
|
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
|
|
|
searchCards: (query: string, limit = 20) =>
|
|
|
|
|
invoke<CardRef[]>("search_cards", { query, limit }),
|
|
|
|
|
|
|
|
|
|
addOwned: (oracleId: string, delta: number) =>
|
|
|
|
|
invoke<number>("add_owned", { oracleId, delta }),
|
|
|
|
|
setOwned: (oracleId: string, quantity: number) =>
|
|
|
|
|
invoke<number>("set_owned", { oracleId, quantity }),
|
|
|
|
|
setLocation: (oracleId: string, location: string) =>
|
|
|
|
|
invoke<void>("set_location", { oracleId, location }),
|
|
|
|
|
|
|
|
|
|
searchLibrary: (filter: LibraryFilter) =>
|
|
|
|
|
invoke<LibraryCard[]>("search_library", { filter }),
|
2026-07-22 20:55:17 -05:00
|
|
|
searchReference: (filter: LibraryFilter, limit: number) =>
|
|
|
|
|
invoke<LibraryCard[]>("search_reference", { filter, limit }),
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
getLibraryStats: () => invoke<LibraryStats>("get_library_stats"),
|
2026-07-08 23:54:47 -05:00
|
|
|
getLibraryFacets: () => invoke<LibraryFacets>("get_library_facets"),
|
2026-07-22 20:55:17 -05:00
|
|
|
getReferenceFacets: () => invoke<LibraryFacets>("get_reference_facets"),
|
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
|
|
|
|
|
|
|
|
listDecks: () => invoke<Deck[]>("list_decks"),
|
|
|
|
|
createDeck: (name: string, commanderOracleId: string | null) =>
|
|
|
|
|
invoke<Deck>("create_deck", { name, commanderOracleId }),
|
|
|
|
|
deleteDeck: (deckId: number) => invoke<void>("delete_deck", { deckId }),
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
renameDeck: (deckId: number, name: string) => invoke<void>("rename_deck", { deckId, name }),
|
|
|
|
|
duplicateDeck: (deckId: number, newName: string) =>
|
|
|
|
|
invoke<Deck>("duplicate_deck", { deckId, newName }),
|
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
|
|
|
setDeckFormat: (deckId: number, format: string) =>
|
|
|
|
|
invoke<void>("set_deck_format", { deckId, format }),
|
|
|
|
|
setDeckCommander: (deckId: number, oracleId: string | null) =>
|
|
|
|
|
invoke<void>("set_deck_commander", { deckId, oracleId }),
|
|
|
|
|
setDeckCard: (
|
|
|
|
|
deckId: number,
|
|
|
|
|
oracleId: string,
|
|
|
|
|
quantity: number,
|
|
|
|
|
isCommander = false,
|
2026-07-14 21:32:25 -05:00
|
|
|
isMaybeboard = false,
|
|
|
|
|
) => invoke<void>("set_deck_card", { deckId, oracleId, quantity, isCommander, isMaybeboard }),
|
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
|
|
|
deckContents: (deckId: number) => invoke<DeckEntry[]>("deck_contents", { deckId }),
|
2026-07-21 21:04:36 -05:00
|
|
|
suggestCards: (deckId: number, offset = 0, unusual = false) =>
|
|
|
|
|
invoke<CardSuggestion[]>("suggest_cards", { deckId, offset, unusual }),
|
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
|
|
|
cardDecks: (oracleId: string) => invoke<Deck[]>("card_decks", { oracleId }),
|
2026-07-20 00:21:22 -05:00
|
|
|
|
|
|
|
|
exportDeck: (deckId: number, path: string) => invoke<void>("export_deck", { deckId, path }),
|
|
|
|
|
importDeck: (path: string, name: string) =>
|
|
|
|
|
invoke<DeckImportResult>("import_deck", { path, name }),
|
|
|
|
|
exportLibraryCards: (oracleIds: string[], path: string) =>
|
|
|
|
|
invoke<void>("export_library_cards", { oracleIds, path }),
|
|
|
|
|
importLibraryCards: (path: string) =>
|
|
|
|
|
invoke<LibraryImportResult>("import_library_cards", { path }),
|
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
|
|
|
};
|