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;
|
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 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;
|
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. 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;
|
Let Android pick which file in the synced folder is the library
Android grants a *folder* through SAF, not a file, and the document name was
hardcoded to "library.json". A library named anything else was therefore never
found: the phone created an empty second library beside it and the two devices
silently never synced, while appearing to work.
The Info tab now lists the .json files in the granted folder and lets you choose
one. Picking an existing file adopts it — the working copy is dropped and
rebuilt from it, matching what desktop does when opening a document — while a
name that isn't there yet keeps the current library and publishes it, matching
New Library. The name is validated as a plain filename, since a path would
escape the granted folder that the SAF grant exists to bound.
Two bugs found while testing this on-device:
load_config decided whether a config was the pre-document format by testing
whether document_path or legacy_library_path were set. An Android install has
neither — its folder is a grant, not a path, and there is no legacy library —
so a valid config's document_name was discarded on every launch and the chosen
library file silently reverted to the default. It no longer infers the format
from which fields happen to be populated; every field defaults, and the legacy
library_path is picked up as a separate additive step. Covered by tests,
including one built from the exact Android config shape, because a setting that
quietly resets itself is not something a careful read catches.
The Info tab's sync refresh fired at mount when the app relaunched onto that
tab — the active tab is persisted — which is before setup() calls manage(),
producing "state not managed for field db". Gated on statusLoaded like the
startup refresh already was; get_status retries until the backend is ready, so
waiting on it is what guarantees the state exists.
2026-08-09 12:17:57 -05:00
|
|
|
/** 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;
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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"),
|
Let Android pick which file in the synced folder is the library
Android grants a *folder* through SAF, not a file, and the document name was
hardcoded to "library.json". A library named anything else was therefore never
found: the phone created an empty second library beside it and the two devices
silently never synced, while appearing to work.
The Info tab now lists the .json files in the granted folder and lets you choose
one. Picking an existing file adopts it — the working copy is dropped and
rebuilt from it, matching what desktop does when opening a document — while a
name that isn't there yet keeps the current library and publishes it, matching
New Library. The name is validated as a plain filename, since a path would
escape the granted folder that the SAF grant exists to bound.
Two bugs found while testing this on-device:
load_config decided whether a config was the pre-document format by testing
whether document_path or legacy_library_path were set. An Android install has
neither — its folder is a grant, not a path, and there is no legacy library —
so a valid config's document_name was discarded on every launch and the chosen
library file silently reverted to the default. It no longer infers the format
from which fields happen to be populated; every field defaults, and the legacy
library_path is picked up as a separate additive step. Covered by tests,
including one built from the exact Android config shape, because a setting that
quietly resets itself is not something a careful read catches.
The Info tab's sync refresh fired at mount when the app relaunched onto that
tab — the active tab is persisted — which is before setup() calls manage(),
producing "state not managed for field db". Gated on statusLoaded like the
startup refresh already was; get_status retries until the backend is ready, so
waiting on it is what guarantees the state exists.
2026-08-09 12:17:57 -05:00
|
|
|
/** Library files already in the chosen folder, for Android's second step. */
|
|
|
|
|
syncListDocuments: () => invoke<string[]>("sync_list_documents"),
|
|
|
|
|
/** Android: choose which file in the granted folder is the library. */
|
|
|
|
|
syncSetDocumentName: (name: string) =>
|
|
|
|
|
invoke<SyncStatus>("sync_set_document_name", { name }),
|
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
|
|
|
/** 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
|
|
|
};
|