ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
import { listen } from "@tauri-apps/api/event";
|
2026-07-20 00:21:22 -05:00
|
|
|
|
import { open, save } from "@tauri-apps/plugin-dialog";
|
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
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
|
import {
|
|
|
|
|
|
api,
|
|
|
|
|
|
Deck,
|
|
|
|
|
|
LibraryCard,
|
|
|
|
|
|
LibraryFacets,
|
|
|
|
|
|
LibraryFilter,
|
|
|
|
|
|
LibraryImportResult,
|
|
|
|
|
|
PreviewContext,
|
|
|
|
|
|
} from "../api";
|
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
|
|
|
|
import {
|
|
|
|
|
|
CardThumb,
|
|
|
|
|
|
CardTile,
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
DialogFrame,
|
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
|
|
|
|
ColorPips,
|
|
|
|
|
|
ColorToggles,
|
|
|
|
|
|
GroupBySelect,
|
|
|
|
|
|
GroupMode,
|
2026-07-20 00:21:22 -05:00
|
|
|
|
InfoDialog,
|
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
|
|
|
|
LocationField,
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
ProgressDialog,
|
2026-07-08 08:20:09 -05:00
|
|
|
|
PromptDialog,
|
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
|
|
|
|
QtyStepper,
|
2026-07-22 20:55:17 -05:00
|
|
|
|
SortBySelect,
|
|
|
|
|
|
SortDir,
|
|
|
|
|
|
SortMode,
|
2026-07-08 23:54:47 -05:00
|
|
|
|
TagInput,
|
2026-10-16 21:14:43 -05:00
|
|
|
|
TileColumnsControl,
|
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
|
|
|
|
ViewToggle,
|
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
|
|
|
|
CardCriteriaFields,
|
|
|
|
|
|
applyCardCriteria,
|
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
|
|
|
|
groupCards,
|
2026-07-22 20:55:17 -05:00
|
|
|
|
sortCards,
|
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
|
|
|
|
useCardCriteria,
|
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
|
|
|
|
usePersistedState,
|
2026-10-16 21:14:43 -05:00
|
|
|
|
useTileColumns,
|
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
|
|
|
|
useViewMode,
|
|
|
|
|
|
} from "../components";
|
|
|
|
|
|
|
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
|
|
|
|
// How long a card that's been decremented to 0 lingers (dimmed, still
|
|
|
|
|
|
// adjustable) before actually dropping out of the library view — gives the
|
|
|
|
|
|
// user a window to undo an accidental removal by clicking + again.
|
|
|
|
|
|
const REMOVAL_GRACE_MS = 5000;
|
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
|
const NO_FACETS: LibraryFacets = { types: [], subtypes: [], keywords: [] };
|
|
|
|
|
|
|
2026-07-21 21:04:36 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Browse & filter the owned library by name and card criteria.
|
|
|
|
|
|
*
|
|
|
|
|
|
* `onOfferUndo` is threaded in from `App` (rather than owning an `UndoBar`
|
|
|
|
|
|
* here) so the resulting "Undo" is also reachable from the command palette,
|
|
|
|
|
|
* which is mounted at the app level and outlives this view across tab
|
|
|
|
|
|
* switches.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function LibraryBrowser({
|
|
|
|
|
|
onOfferUndo,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
onOfferUndo: (message: string, run: () => void | Promise<void>) => void;
|
|
|
|
|
|
}) {
|
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
|
|
|
|
const [text, setText] = useState("");
|
|
|
|
|
|
const [commander, setCommander] = useState(0);
|
|
|
|
|
|
const [colorsInclude, setColorsInclude] = useState(0);
|
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
|
|
|
|
// Colourlessness can't ride in the bitmask — 0 there means "no filter".
|
|
|
|
|
|
const [colorlessIdentity, setColorlessIdentity] = useState(false);
|
|
|
|
|
|
const [colorlessColors, setColorlessColors] = useState(false);
|
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
|
|
|
|
const [cmcMin, setCmcMin] = useState("");
|
|
|
|
|
|
const [cmcMax, setCmcMax] = useState("");
|
2026-07-08 23:54:47 -05:00
|
|
|
|
const [types, setTypes] = useState<string[]>([]);
|
|
|
|
|
|
const [subtypes, setSubtypes] = useState<string[]>([]);
|
|
|
|
|
|
const [keywords, setKeywords] = useState<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
|
|
|
|
const [inDeck, setInDeck] = useState<number | "">("");
|
2026-07-07 21:35:09 -05:00
|
|
|
|
const [location, setLocation] = useState("");
|
2026-07-08 23:54:47 -05:00
|
|
|
|
const [oracleText, setOracleText] = useState("");
|
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
|
|
|
|
const [criteria, patchCriteria] = useCardCriteria("library-criteria");
|
2026-08-02 16:10:25 -05:00
|
|
|
|
// Collapsed by default (relevant on narrow/mobile screens only — see
|
|
|
|
|
|
// .filters-more's CSS, which is always expanded above that breakpoint
|
|
|
|
|
|
// regardless of this).
|
|
|
|
|
|
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
2026-07-08 23:54:47 -05:00
|
|
|
|
const [facets, setFacets] = useState<LibraryFacets>(NO_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
|
|
|
|
|
|
|
|
|
|
const [decks, setDecks] = useState<Deck[]>([]);
|
|
|
|
|
|
const [rows, setRows] = useState<LibraryCard[]>([]);
|
|
|
|
|
|
const [view, setView] = useViewMode("library-view");
|
2026-10-16 21:14:43 -05:00
|
|
|
|
const tiles = useTileColumns();
|
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
|
|
|
|
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("library-group", "none");
|
2026-07-22 20:55:17 -05:00
|
|
|
|
const [sortBy, setSortBy] = usePersistedState<SortMode>("library-sort", "name");
|
|
|
|
|
|
const [sortDir, setSortDir] = usePersistedState<SortDir>("library-sort-dir", "asc");
|
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
|
|
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
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
|
|
|
|
const [pendingRemoval, setPendingRemoval] = useState<Set<string>>(new Set());
|
|
|
|
|
|
const removalTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
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
|
|
|
|
|
2026-07-08 08:20:09 -05:00
|
|
|
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
|
|
|
|
const [bulkDeckId, setBulkDeckId] = useState<number | "">("");
|
2026-07-20 00:21:22 -05:00
|
|
|
|
const [bulkToMaybeboard, setBulkToMaybeboard] = useState(false);
|
2026-07-08 08:20:09 -05:00
|
|
|
|
const [bulkDelta, setBulkDelta] = useState(1);
|
|
|
|
|
|
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
|
|
|
|
|
|
const [bulkBusy, setBulkBusy] = useState(false);
|
2026-07-20 00:21:22 -05:00
|
|
|
|
const [importResult, setImportResult] = useState<LibraryImportResult | null>(null);
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
const [importError, setImportError] = useState<string | null>(null);
|
|
|
|
|
|
// The "what can I import?" explainer shown before the file picker.
|
|
|
|
|
|
const [importIntroOpen, setImportIntroOpen] = useState(false);
|
2026-10-17 21:58:44 -05:00
|
|
|
|
// The file picked to import, while asking where its cards are kept.
|
|
|
|
|
|
const [importPath, setImportPath] = useState<string | null>(null);
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
// Lines done / total while an import runs; null when idle.
|
|
|
|
|
|
const [importProgress, setImportProgress] = useState<{ done: number; total: number } | null>(null);
|
|
|
|
|
|
// Only show the progress dialog for an import that's taking a while, so a
|
|
|
|
|
|
// quick one doesn't flash it up.
|
|
|
|
|
|
const [showImportProgress, setShowImportProgress] = useState(false);
|
2026-07-08 08:20:09 -05:00
|
|
|
|
const selectAllRef = useRef<HTMLInputElement>(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
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
api.listDecks().then(setDecks);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
|
// Refetch whenever refreshKey bumps too, so newly-owned cards' types
|
|
|
|
|
|
// become suggestible without restarting the app.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
api.getLibraryFacets().then(setFacets).catch(() => setFacets(NO_FACETS));
|
|
|
|
|
|
}, [refreshKey]);
|
|
|
|
|
|
|
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
|
|
|
|
// Cancel any in-flight removal timers on unmount so they don't fire against
|
|
|
|
|
|
// an unmounted component.
|
|
|
|
|
|
useEffect(() => () => removalTimers.current.forEach(clearTimeout), []);
|
|
|
|
|
|
|
2026-07-08 08:20:09 -05:00
|
|
|
|
// Keep selection in sync with what's actually visible — drop ids that fall
|
|
|
|
|
|
// out of the filtered set (filter changed, or a card's qty hit 0 and its
|
|
|
|
|
|
// grace period expired).
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setSelected((s) => {
|
|
|
|
|
|
const visible = new Set(rows.map((r) => r.oracle_id));
|
|
|
|
|
|
const next = new Set([...s].filter((id) => visible.has(id)));
|
|
|
|
|
|
return next.size === s.size ? s : next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [rows]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectAllRef.current) {
|
|
|
|
|
|
selectAllRef.current.indeterminate = selected.size > 0 && selected.size < rows.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selected, rows]);
|
|
|
|
|
|
|
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
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const filter: LibraryFilter = {
|
|
|
|
|
|
text: text.trim() || null,
|
|
|
|
|
|
commander_identity: commander || null,
|
|
|
|
|
|
colors_include: colorsInclude || null,
|
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
|
|
|
|
colorless_identity: colorlessIdentity,
|
|
|
|
|
|
colorless_colors: colorlessColors,
|
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
|
|
|
|
cmc_min: cmcMin === "" ? null : Number(cmcMin),
|
|
|
|
|
|
cmc_max: cmcMax === "" ? null : Number(cmcMax),
|
2026-07-08 23:54:47 -05:00
|
|
|
|
types,
|
|
|
|
|
|
subtypes,
|
|
|
|
|
|
keywords,
|
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
|
|
|
|
in_deck: inDeck === "" ? null : Number(inDeck),
|
2026-07-07 21:35:09 -05:00
|
|
|
|
location: location.trim() || null,
|
2026-07-08 23:54:47 -05:00
|
|
|
|
oracle_text: oracleText.trim() || 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
|
|
|
|
...applyCardCriteria(criteria),
|
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
|
|
|
|
};
|
|
|
|
|
|
const t = setTimeout(() => {
|
|
|
|
|
|
api.searchLibrary(filter).then(setRows).catch(() => setRows([]));
|
|
|
|
|
|
}, 120);
|
|
|
|
|
|
return () => clearTimeout(t);
|
2026-07-07 21:35:09 -05:00
|
|
|
|
}, [
|
|
|
|
|
|
text,
|
|
|
|
|
|
commander,
|
|
|
|
|
|
colorsInclude,
|
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
|
|
|
|
colorlessIdentity,
|
|
|
|
|
|
colorlessColors,
|
2026-07-07 21:35:09 -05:00
|
|
|
|
cmcMin,
|
|
|
|
|
|
cmcMax,
|
2026-07-08 23:54:47 -05:00
|
|
|
|
types,
|
|
|
|
|
|
subtypes,
|
|
|
|
|
|
keywords,
|
2026-07-07 21:35:09 -05:00
|
|
|
|
inDeck,
|
|
|
|
|
|
location,
|
2026-07-08 23:54:47 -05:00
|
|
|
|
oracleText,
|
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
|
|
|
|
criteria,
|
2026-07-07 21:35:09 -05:00
|
|
|
|
refreshKey,
|
|
|
|
|
|
]);
|
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
|
|
|
|
|
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
|
|
|
|
async function adjust(oracleId: string, delta: number) {
|
|
|
|
|
|
const qty = await api.addOwned(oracleId, delta);
|
|
|
|
|
|
setRows((rs) => rs.map((c) => (c.oracle_id === oracleId ? { ...c, quantity_owned: qty } : c)));
|
|
|
|
|
|
|
|
|
|
|
|
// Re-adding during the grace period below cancels the pending removal.
|
|
|
|
|
|
const timer = removalTimers.current.get(oracleId);
|
|
|
|
|
|
if (timer) {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
removalTimers.current.delete(oracleId);
|
|
|
|
|
|
setPendingRemoval((p) => {
|
|
|
|
|
|
const next = new Set(p);
|
|
|
|
|
|
next.delete(oracleId);
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (qty <= 0) {
|
|
|
|
|
|
// search_library only returns owned (>0) cards — rather than dropping
|
|
|
|
|
|
// the row immediately, dim it and give the user a window to undo a
|
|
|
|
|
|
// mistaken decrement (via +) before it actually disappears.
|
|
|
|
|
|
setPendingRemoval((p) => new Set(p).add(oracleId));
|
|
|
|
|
|
removalTimers.current.set(
|
|
|
|
|
|
oracleId,
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setRows((rs) => rs.filter((c) => c.oracle_id !== oracleId));
|
|
|
|
|
|
setPendingRemoval((p) => {
|
|
|
|
|
|
const next = new Set(p);
|
|
|
|
|
|
next.delete(oracleId);
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
removalTimers.current.delete(oracleId);
|
|
|
|
|
|
}, REMOVAL_GRACE_MS),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 08:20:09 -05:00
|
|
|
|
function toggleOne(oracleId: string) {
|
|
|
|
|
|
setSelected((s) => {
|
|
|
|
|
|
const next = new Set(s);
|
|
|
|
|
|
if (next.has(oracleId)) next.delete(oracleId);
|
|
|
|
|
|
else next.add(oracleId);
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleSelectAll() {
|
|
|
|
|
|
setSelected((s) => (s.size === rows.length ? new Set() : new Set(rows.map((r) => r.oracle_id))));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set the in-deck quantity of every selected card to either 1 or the full
|
|
|
|
|
|
* owned count (e.g. select a handful of basic lands, "Add All" to put
|
|
|
|
|
|
* every copy you own into the deck at once) — overwrites the quantity for
|
|
|
|
|
|
* cards already in the deck rather than skipping them, so it doubles as a
|
2026-07-20 00:21:22 -05:00
|
|
|
|
* bulk "fix the land count" tool. `bulkToMaybeboard` sends them to the
|
|
|
|
|
|
* deck's maybeboard instead of the mainboard.
|
2026-07-21 21:04:36 -05:00
|
|
|
|
*
|
|
|
|
|
|
* Since this overwrites rather than skips, it can clobber an existing
|
|
|
|
|
|
* in-deck quantity/board — so it snapshots each target's prior deck_cards
|
|
|
|
|
|
* row (or absence of one) first and offers an "Undo" that restores exactly
|
|
|
|
|
|
* that prior state (deleting the row again for cards that weren't in the
|
|
|
|
|
|
* deck before).
|
2026-07-08 08:20:09 -05:00
|
|
|
|
*/
|
|
|
|
|
|
async function bulkAddToDeck(mode: "one" | "all") {
|
|
|
|
|
|
if (bulkDeckId === "") return;
|
|
|
|
|
|
const deck = decks.find((d) => d.id === bulkDeckId);
|
|
|
|
|
|
if (!deck) return;
|
|
|
|
|
|
setBulkBusy(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const targets = rows.filter((r) => selected.has(r.oracle_id));
|
2026-07-21 21:04:36 -05:00
|
|
|
|
const before = await api.deckContents(deck.id);
|
|
|
|
|
|
const prevByOracle = new Map(before.map((e) => [e.oracle_id, e]));
|
2026-07-08 08:20:09 -05:00
|
|
|
|
await Promise.all(
|
|
|
|
|
|
targets.map((r) =>
|
2026-07-20 00:21:22 -05:00
|
|
|
|
api.setDeckCard(
|
|
|
|
|
|
deck.id,
|
|
|
|
|
|
r.oracle_id,
|
|
|
|
|
|
mode === "all" ? r.quantity_owned : 1,
|
|
|
|
|
|
false,
|
|
|
|
|
|
bulkToMaybeboard,
|
|
|
|
|
|
),
|
2026-07-08 08:20:09 -05:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
setRefreshKey((k) => k + 1);
|
2026-07-21 21:04:36 -05:00
|
|
|
|
onOfferUndo(
|
|
|
|
|
|
`Added ${targets.length} card${targets.length === 1 ? "" : "s"} to ${deck.name}.`,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
|
targets.map((r) => {
|
|
|
|
|
|
const prev = prevByOracle.get(r.oracle_id);
|
|
|
|
|
|
return api.setDeckCard(
|
|
|
|
|
|
deck.id,
|
|
|
|
|
|
r.oracle_id,
|
|
|
|
|
|
prev?.quantity ?? 0,
|
|
|
|
|
|
prev?.is_commander ?? false,
|
|
|
|
|
|
prev?.is_maybeboard ?? false,
|
|
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
setRefreshKey((k) => k + 1);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-07-08 08:20:09 -05:00
|
|
|
|
} finally {
|
|
|
|
|
|
setBulkBusy(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function bulkSetLocation(value: string) {
|
|
|
|
|
|
setBulkBusy(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([...selected].map((id) => api.setLocation(id, value)));
|
|
|
|
|
|
setLocationDialogOpen(false);
|
|
|
|
|
|
setRefreshKey((k) => k + 1);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBulkBusy(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function bulkAdjust(delta: number) {
|
|
|
|
|
|
setBulkBusy(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([...selected].map((id) => adjust(id, delta)));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBulkBusy(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
|
async function exportSelected() {
|
|
|
|
|
|
const targets = rows.filter((r) => selected.has(r.oracle_id)).map((r) => r.oracle_id);
|
|
|
|
|
|
if (targets.length === 0) return;
|
|
|
|
|
|
const path = await save({
|
|
|
|
|
|
title: "Export library cards",
|
|
|
|
|
|
defaultPath: "library-export.txt",
|
|
|
|
|
|
filters: [{ name: "Decklist", extensions: ["txt"] }],
|
|
|
|
|
|
});
|
|
|
|
|
|
if (typeof path === "string") await api.exportLibraryCards(targets, path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pickImportFile() {
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
setImportIntroOpen(false);
|
2026-07-20 00:21:22 -05:00
|
|
|
|
const path = await open({
|
|
|
|
|
|
title: "Import library cards",
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
filters: [{ name: "Card list (decklist or ManaBox export)", extensions: ["txt", "csv"] }],
|
2026-07-20 00:21:22 -05:00
|
|
|
|
});
|
2026-10-17 21:58:44 -05:00
|
|
|
|
if (typeof path === "string") setImportPath(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runImport(path: string, location: string) {
|
|
|
|
|
|
setImportPath(null);
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
setImportProgress({ done: 0, total: 0 });
|
|
|
|
|
|
const reveal = setTimeout(() => setShowImportProgress(true), 300);
|
|
|
|
|
|
const unlisten = await listen<{ done: number; total: number }>("library-import-progress", (e) =>
|
|
|
|
|
|
setImportProgress(e.payload),
|
|
|
|
|
|
);
|
|
|
|
|
|
try {
|
2026-10-17 21:58:44 -05:00
|
|
|
|
setImportResult(await api.importLibraryCards(path, location));
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setImportError(String(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
unlisten();
|
|
|
|
|
|
clearTimeout(reveal);
|
|
|
|
|
|
setShowImportProgress(false);
|
|
|
|
|
|
setImportProgress(null);
|
|
|
|
|
|
setRefreshKey((k) => k + 1);
|
|
|
|
|
|
}
|
2026-07-20 00:21:22 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
const deckOrLocation = (c: LibraryCard) =>
|
|
|
|
|
|
c.decks.length === 0 ? (
|
|
|
|
|
|
<LocationField
|
|
|
|
|
|
oracleId={c.oracle_id}
|
|
|
|
|
|
location={c.location}
|
|
|
|
|
|
onSaved={() => setRefreshKey((k) => k + 1)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
c.decks.map((d) => (
|
|
|
|
|
|
<span key={d} className="deck-chip">
|
|
|
|
|
|
{d}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
|
// Under the spares filter, how many copies are free — only worth saying for
|
|
|
|
|
|
// a card some deck uses, since otherwise it's every copy shown.
|
|
|
|
|
|
const spareNote = (c: LibraryCard) =>
|
|
|
|
|
|
criteria.sparesOnly &&
|
|
|
|
|
|
c.quantity_assigned > 0 && (
|
|
|
|
|
|
<span className="muted" title="Copies no deck is using">
|
|
|
|
|
|
{c.quantity_owned - c.quantity_assigned} spare
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
|
// Lets the click preview track which printings the copies are.
|
|
|
|
|
|
const preview: PreviewContext = { onChanged: () => setRefreshKey((k) => k + 1) };
|
|
|
|
|
|
|
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
|
|
|
|
function libTable(cards: LibraryCard[]) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-07-08 08:20:09 -05:00
|
|
|
|
<th></th>
|
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
|
|
|
|
<th></th>
|
|
|
|
|
|
<th>Colors</th>
|
|
|
|
|
|
<th>Name</th>
|
|
|
|
|
|
<th>Type</th>
|
|
|
|
|
|
<th>MV</th>
|
|
|
|
|
|
<th>Owned</th>
|
|
|
|
|
|
<th>Decks / Location</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{cards.map((c) => (
|
|
|
|
|
|
<tr key={c.oracle_id}>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
<td className="cell-check">
|
2026-07-08 08:20:09 -05:00
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={selected.has(c.oracle_id)}
|
|
|
|
|
|
onChange={() => toggleOne(c.oracle_id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</td>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
<td className="cell-thumb">
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
|
<CardThumb card={c} preview={preview} />
|
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
|
|
|
|
</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<ColorPips mask={c.color_identity} />
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="card-name">{c.name}</td>
|
|
|
|
|
|
<td className="card-type">{c.type_line}</td>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
<td data-label="MV">{c.mana_value}</td>
|
|
|
|
|
|
<td
|
|
|
|
|
|
data-label="Owned"
|
|
|
|
|
|
className={c.quantity_assigned > c.quantity_owned ? "over-assigned" : ""}
|
|
|
|
|
|
>
|
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
|
|
|
|
<span className="row" style={{ gap: 8 }}>
|
|
|
|
|
|
<QtyStepper
|
|
|
|
|
|
value={c.quantity_owned}
|
|
|
|
|
|
onDecrement={() => adjust(c.oracle_id, -1)}
|
|
|
|
|
|
onIncrement={() => adjust(c.oracle_id, 1)}
|
|
|
|
|
|
warn={c.quantity_assigned > c.quantity_owned}
|
|
|
|
|
|
pending={pendingRemoval.has(c.oracle_id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{c.quantity_assigned > c.quantity_owned && (
|
|
|
|
|
|
<span title="Assigned to more decks than you own">
|
|
|
|
|
|
⚠ {c.quantity_assigned} assigned
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
|
{spareNote(c)}
|
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
|
|
|
|
</span>
|
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
|
|
|
|
</td>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
<td data-label="Decks / Location">{deckOrLocation(c)}</td>
|
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
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function libTiles(cards: LibraryCard[]) {
|
|
|
|
|
|
return (
|
2026-10-16 21:14:43 -05:00
|
|
|
|
<div {...tiles.gridProps}>
|
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
|
|
|
|
{cards.map((c) => (
|
|
|
|
|
|
<CardTile
|
|
|
|
|
|
key={c.oracle_id}
|
|
|
|
|
|
card={c}
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
|
preview={preview}
|
2026-07-08 08:20:09 -05:00
|
|
|
|
selected={selected.has(c.oracle_id)}
|
|
|
|
|
|
cornerLeft={
|
|
|
|
|
|
<span className="tile-select">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={selected.has(c.oracle_id)}
|
|
|
|
|
|
onChange={() => toggleOne(c.oracle_id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
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
|
|
|
|
corner={
|
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
|
|
|
|
<QtyStepper
|
|
|
|
|
|
overlay
|
|
|
|
|
|
value={c.quantity_owned}
|
|
|
|
|
|
onDecrement={() => adjust(c.oracle_id, -1)}
|
|
|
|
|
|
onIncrement={() => adjust(c.oracle_id, 1)}
|
|
|
|
|
|
warn={c.quantity_assigned > c.quantity_owned}
|
|
|
|
|
|
pending={pendingRemoval.has(c.oracle_id)}
|
|
|
|
|
|
/>
|
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
|
|
|
|
}
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
|
footer={
|
|
|
|
|
|
<>
|
|
|
|
|
|
{deckOrLocation(c)}
|
|
|
|
|
|
{spareNote(c)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
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
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 20:55:17 -05:00
|
|
|
|
const groups = groupCards(sortCards(rows, sortBy, sortDir), groupBy);
|
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
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
Cohesive styling pass: design tokens, shared primitives, responsive groundwork
The UI had a good terracotta-on-dark system that wasn't applied
consistently — magic-number spacing, ad-hoc page headers, ~6 copied
panel surfaces, ~8 drifting label variants, and scattered accent use.
- Tokens: warm the neutral ramp, add spacing/radius/type/motion scales,
--accent-quiet + panel highlight. Terracotta hue unchanged.
- Primitives: .panel, .section-label, .page-header/.toolbar — collapse
the copied surfaces and label variants onto shared classes.
- Unify each filter-heavy page's display controls (group/sort/view,
select-all, count) into a workspace toolbar inside the filter panel,
so they read as one surface instead of floating above it.
- Labels: sentence-case, higher-contrast form labels; uppercase section
markers bumped faint -> muted.
- Accent hierarchy: reserve full terracotta for primary actions/active
state; demote decorative labels and the group-header underline.
- Responsive groundwork: reflow classes for the deck two-pane and nav
rail (stacks / icon-collapse at breakpoints), touch-target sizing.
Desktop unchanged in kind.
2026-07-22 23:44:21 -05:00
|
|
|
|
<div className="page-header">
|
|
|
|
|
|
<h2 className="page-header-title">
|
|
|
|
|
|
Library <span className="badge">{rows.length}</span>
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div className="page-header-actions">
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setImportIntroOpen(true)}
|
|
|
|
|
|
disabled={importProgress !== null}
|
|
|
|
|
|
>
|
|
|
|
|
|
Import cards…
|
|
|
|
|
|
</button>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-08 08:20:09 -05:00
|
|
|
|
{selected.size > 0 && (
|
|
|
|
|
|
<div className="bulk-bar">
|
|
|
|
|
|
<span className="bulk-bar-count">{selected.size} selected</span>
|
|
|
|
|
|
<div className="bulk-bar-actions">
|
|
|
|
|
|
<span className="row" style={{ gap: 6 }}>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={bulkDeckId}
|
|
|
|
|
|
onChange={(e) => setBulkDeckId(e.target.value === "" ? "" : Number(e.target.value))}
|
|
|
|
|
|
disabled={bulkBusy}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Add to deck…</option>
|
|
|
|
|
|
{decks.map((d) => (
|
|
|
|
|
|
<option key={d.id} value={d.id}>
|
|
|
|
|
|
{d.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => bulkAddToDeck("one")}
|
|
|
|
|
|
disabled={bulkBusy || bulkDeckId === ""}
|
|
|
|
|
|
title="Set each selected card to 1 copy in the deck"
|
|
|
|
|
|
>
|
|
|
|
|
|
Add 1
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => bulkAddToDeck("all")}
|
|
|
|
|
|
disabled={bulkBusy || bulkDeckId === ""}
|
|
|
|
|
|
title="Set each selected card to every copy you own"
|
|
|
|
|
|
>
|
|
|
|
|
|
Add All
|
|
|
|
|
|
</button>
|
2026-07-20 00:21:22 -05:00
|
|
|
|
<label className="row" style={{ gap: 4, cursor: "pointer" }} title="Send to the maybeboard instead of the mainboard">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={bulkToMaybeboard}
|
|
|
|
|
|
onChange={(e) => setBulkToMaybeboard(e.target.checked)}
|
|
|
|
|
|
disabled={bulkBusy}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="muted" style={{ fontSize: 12 }}>
|
|
|
|
|
|
Maybeboard
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
2026-07-08 08:20:09 -05:00
|
|
|
|
</span>
|
|
|
|
|
|
<button onClick={() => setLocationDialogOpen(true)} disabled={bulkBusy}>
|
|
|
|
|
|
Set location…
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span className="row" style={{ gap: 4 }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => bulkAdjust(-bulkDelta)}
|
|
|
|
|
|
disabled={bulkBusy}
|
|
|
|
|
|
title="Remove copies from each selected card"
|
|
|
|
|
|
>
|
|
|
|
|
|
−
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className="bulk-bar-delta"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
value={bulkDelta}
|
|
|
|
|
|
onChange={(e) => setBulkDelta(Math.max(1, Number(e.target.value) || 1))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => bulkAdjust(bulkDelta)}
|
|
|
|
|
|
disabled={bulkBusy}
|
|
|
|
|
|
title="Add copies to each selected card"
|
|
|
|
|
|
>
|
|
|
|
|
|
+
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
2026-07-20 00:21:22 -05:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={exportSelected}
|
|
|
|
|
|
disabled={bulkBusy}
|
|
|
|
|
|
title="Export the selected cards to a decklist file"
|
|
|
|
|
|
>
|
|
|
|
|
|
Export selected
|
|
|
|
|
|
</button>
|
2026-07-08 08:20:09 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
<span className="spacer" />
|
|
|
|
|
|
<button onClick={() => setSelected(new Set())} disabled={bulkBusy}>
|
|
|
|
|
|
Clear
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{locationDialogOpen && (
|
|
|
|
|
|
<PromptDialog
|
|
|
|
|
|
title="Set location"
|
|
|
|
|
|
label={`Location for ${selected.size} card${selected.size === 1 ? "" : "s"}`}
|
|
|
|
|
|
defaultValue=""
|
|
|
|
|
|
confirmLabel="Apply"
|
|
|
|
|
|
onConfirm={bulkSetLocation}
|
|
|
|
|
|
onCancel={() => setLocationDialogOpen(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
{importIntroOpen && (
|
|
|
|
|
|
<DialogFrame title="Import cards" onCancel={() => setImportIntroOpen(false)} wide>
|
|
|
|
|
|
<p className="dialog-message">
|
|
|
|
|
|
Adds the cards in a file to your library. Nothing you already own is replaced or
|
|
|
|
|
|
removed. You can import:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="dialog-list">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>A card list</strong> (.txt), one card per line, like <code>4 Lightning Bolt</code>.
|
|
|
|
|
|
A line can name a printing, like <code>1 Sol Ring (2X2) 315 *F*</code>, where{" "}
|
|
|
|
|
|
<code>*F*</code> is foil and <code>*E*</code> etched. Library export, Moxfield and
|
|
|
|
|
|
Arena all write this format.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>A ManaBox export</strong> of a collection or binder, either the .csv or the
|
|
|
|
|
|
.txt. The CSV records each copy's exact printing and finish, so it's the better
|
|
|
|
|
|
choice. ManaBox's condition, language and price fields aren't kept.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
2026-10-17 21:58:44 -05:00
|
|
|
|
<p className="dialog-message">
|
|
|
|
|
|
After you choose a file, you can file its cards under a location, like the binder they
|
|
|
|
|
|
came from. Any cards that can't be matched are listed afterwards.
|
|
|
|
|
|
</p>
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
<div className="dialog-actions">
|
|
|
|
|
|
<button onClick={() => setImportIntroOpen(false)}>Cancel</button>
|
|
|
|
|
|
<button className="primary" onClick={pickImportFile} autoFocus>
|
|
|
|
|
|
Choose file…
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</DialogFrame>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-10-17 21:58:44 -05:00
|
|
|
|
{importPath && (
|
|
|
|
|
|
<ImportLocationDialog
|
|
|
|
|
|
path={importPath}
|
|
|
|
|
|
onImport={(location) => runImport(importPath, location)}
|
|
|
|
|
|
onCancel={() => setImportPath(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
{importProgress && showImportProgress && (
|
|
|
|
|
|
<ProgressDialog
|
|
|
|
|
|
title="Importing cards…"
|
|
|
|
|
|
done={importProgress.done}
|
|
|
|
|
|
total={importProgress.total}
|
|
|
|
|
|
unit="lines"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{importError && (
|
|
|
|
|
|
<InfoDialog
|
|
|
|
|
|
title="Couldn't import that file"
|
|
|
|
|
|
message={importError}
|
|
|
|
|
|
onClose={() => setImportError(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
|
{importResult && (
|
|
|
|
|
|
<InfoDialog
|
|
|
|
|
|
title="Cards imported"
|
|
|
|
|
|
message={
|
|
|
|
|
|
importResult.unresolved.length > 0
|
|
|
|
|
|
? `Added ${importResult.added.length} card${importResult.added.length === 1 ? "" : "s"}. ${importResult.unresolved.length} name${importResult.unresolved.length === 1 ? "" : "s"} couldn't be matched:`
|
|
|
|
|
|
: `Added ${importResult.added.length} card${importResult.added.length === 1 ? "" : "s"} to your library.`
|
|
|
|
|
|
}
|
|
|
|
|
|
items={importResult.unresolved}
|
|
|
|
|
|
onClose={() => setImportResult(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Cohesive styling pass: design tokens, shared primitives, responsive groundwork
The UI had a good terracotta-on-dark system that wasn't applied
consistently — magic-number spacing, ad-hoc page headers, ~6 copied
panel surfaces, ~8 drifting label variants, and scattered accent use.
- Tokens: warm the neutral ramp, add spacing/radius/type/motion scales,
--accent-quiet + panel highlight. Terracotta hue unchanged.
- Primitives: .panel, .section-label, .page-header/.toolbar — collapse
the copied surfaces and label variants onto shared classes.
- Unify each filter-heavy page's display controls (group/sort/view,
select-all, count) into a workspace toolbar inside the filter panel,
so they read as one surface instead of floating above it.
- Labels: sentence-case, higher-contrast form labels; uppercase section
markers bumped faint -> muted.
- Accent hierarchy: reserve full terracotta for primary actions/active
state; demote decorative labels and the group-header underline.
- Responsive groundwork: reflow classes for the deck two-pane and nav
rail (stacks / icon-collapse at breakpoints), touch-target sizing.
Desktop unchanged in kind.
2026-07-22 23:44:21 -05:00
|
|
|
|
<div className="panel workspace">
|
|
|
|
|
|
<div className="filters">
|
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
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Name contains</label>
|
|
|
|
|
|
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. bolt" />
|
|
|
|
|
|
</div>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
<div className={`filters-more ${filtersExpanded ? "expanded" : ""}`}>
|
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
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Playable in commander (color identity ⊆)</label>
|
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
|
|
|
|
<ColorToggles
|
|
|
|
|
|
value={commander}
|
|
|
|
|
|
onChange={setCommander}
|
|
|
|
|
|
colorless={colorlessIdentity}
|
|
|
|
|
|
onColorlessChange={setColorlessIdentity}
|
|
|
|
|
|
colorlessTitle="Colorless commander — only cards with no color identity"
|
|
|
|
|
|
/>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Includes colors</label>
|
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
|
|
|
|
<ColorToggles
|
|
|
|
|
|
value={colorsInclude}
|
|
|
|
|
|
onChange={setColorsInclude}
|
|
|
|
|
|
colorless={colorlessColors}
|
|
|
|
|
|
onColorlessChange={setColorlessColors}
|
|
|
|
|
|
colorlessTitle="Colorless cards only"
|
|
|
|
|
|
/>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Mana value</label>
|
|
|
|
|
|
<div className="row">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
style={{ width: 70 }}
|
|
|
|
|
|
placeholder="min"
|
|
|
|
|
|
value={cmcMin}
|
|
|
|
|
|
onChange={(e) => setCmcMin(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="muted">–</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
style={{ width: 70 }}
|
|
|
|
|
|
placeholder="max"
|
|
|
|
|
|
value={cmcMax}
|
|
|
|
|
|
onChange={(e) => setCmcMax(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Type(s)</label>
|
2026-07-08 23:54:47 -05:00
|
|
|
|
<TagInput values={types} onChange={setTypes} suggestions={facets.types} placeholder="Creature…" />
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Subtype(s)</label>
|
2026-07-08 23:54:47 -05:00
|
|
|
|
<TagInput
|
|
|
|
|
|
values={subtypes}
|
|
|
|
|
|
onChange={setSubtypes}
|
|
|
|
|
|
suggestions={facets.subtypes}
|
|
|
|
|
|
placeholder="Elf…"
|
|
|
|
|
|
/>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Keyword(s)</label>
|
2026-07-08 23:54:47 -05:00
|
|
|
|
<TagInput
|
|
|
|
|
|
values={keywords}
|
|
|
|
|
|
onChange={setKeywords}
|
|
|
|
|
|
suggestions={facets.keywords}
|
|
|
|
|
|
placeholder="Flying…"
|
|
|
|
|
|
/>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>In deck</label>
|
|
|
|
|
|
<select value={inDeck} onChange={(e) => setInDeck(e.target.value === "" ? "" : Number(e.target.value))}>
|
|
|
|
|
|
<option value="">Any</option>
|
|
|
|
|
|
{decks.map((d) => (
|
|
|
|
|
|
<option key={d.id} value={d.id}>
|
|
|
|
|
|
{d.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-07-07 21:35:09 -05:00
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Location</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={location}
|
|
|
|
|
|
onChange={(e) => setLocation(e.target.value)}
|
|
|
|
|
|
placeholder="e.g. Box 1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-07-08 23:54:47 -05:00
|
|
|
|
<div className="field">
|
|
|
|
|
|
<label>Oracle text</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={oracleText}
|
|
|
|
|
|
onChange={(e) => setOracleText(e.target.value)}
|
|
|
|
|
|
placeholder="e.g. landfall"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
|
<CardCriteriaFields value={criteria} onChange={patchCriteria} hideNotOwned showSpares />
|
Cohesive styling pass: design tokens, shared primitives, responsive groundwork
The UI had a good terracotta-on-dark system that wasn't applied
consistently — magic-number spacing, ad-hoc page headers, ~6 copied
panel surfaces, ~8 drifting label variants, and scattered accent use.
- Tokens: warm the neutral ramp, add spacing/radius/type/motion scales,
--accent-quiet + panel highlight. Terracotta hue unchanged.
- Primitives: .panel, .section-label, .page-header/.toolbar — collapse
the copied surfaces and label variants onto shared classes.
- Unify each filter-heavy page's display controls (group/sort/view,
select-all, count) into a workspace toolbar inside the filter panel,
so they read as one surface instead of floating above it.
- Labels: sentence-case, higher-contrast form labels; uppercase section
markers bumped faint -> muted.
- Accent hierarchy: reserve full terracotta for primary actions/active
state; demote decorative labels and the group-header underline.
- Responsive groundwork: reflow classes for the deck two-pane and nav
rail (stacks / icon-collapse at breakpoints), touch-target sizing.
Desktop unchanged in kind.
2026-07-22 23:44:21 -05:00
|
|
|
|
</div>
|
2026-08-02 16:10:25 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="filters-toggle"
|
|
|
|
|
|
onClick={() => setFiltersExpanded((v) => !v)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{filtersExpanded ? "▴ Fewer filters" : "▾ More filters"}
|
|
|
|
|
|
</button>
|
Cohesive styling pass: design tokens, shared primitives, responsive groundwork
The UI had a good terracotta-on-dark system that wasn't applied
consistently — magic-number spacing, ad-hoc page headers, ~6 copied
panel surfaces, ~8 drifting label variants, and scattered accent use.
- Tokens: warm the neutral ramp, add spacing/radius/type/motion scales,
--accent-quiet + panel highlight. Terracotta hue unchanged.
- Primitives: .panel, .section-label, .page-header/.toolbar — collapse
the copied surfaces and label variants onto shared classes.
- Unify each filter-heavy page's display controls (group/sort/view,
select-all, count) into a workspace toolbar inside the filter panel,
so they read as one surface instead of floating above it.
- Labels: sentence-case, higher-contrast form labels; uppercase section
markers bumped faint -> muted.
- Accent hierarchy: reserve full terracotta for primary actions/active
state; demote decorative labels and the group-header underline.
- Responsive groundwork: reflow classes for the deck two-pane and nav
rail (stacks / icon-collapse at breakpoints), touch-target sizing.
Desktop unchanged in kind.
2026-07-22 23:44:21 -05:00
|
|
|
|
|
|
|
|
|
|
<div className="workspace-bar">
|
|
|
|
|
|
<label className="workspace-bar-check" title="Select all filtered cards">
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={selectAllRef}
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={rows.length > 0 && selected.size === rows.length}
|
|
|
|
|
|
onChange={toggleSelectAll}
|
|
|
|
|
|
disabled={rows.length === 0}
|
|
|
|
|
|
/>
|
|
|
|
|
|
Select all
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<span className="workspace-bar-count">
|
|
|
|
|
|
{rows.length} card{rows.length === 1 ? "" : "s"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="spacer" />
|
|
|
|
|
|
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
|
|
|
|
|
<SortBySelect
|
|
|
|
|
|
value={sortBy}
|
|
|
|
|
|
dir={sortDir}
|
|
|
|
|
|
onChangeValue={setSortBy}
|
|
|
|
|
|
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ViewToggle mode={view} onChange={setView} />
|
2026-10-16 21:14:43 -05:00
|
|
|
|
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
|
Cohesive styling pass: design tokens, shared primitives, responsive groundwork
The UI had a good terracotta-on-dark system that wasn't applied
consistently — magic-number spacing, ad-hoc page headers, ~6 copied
panel surfaces, ~8 drifting label variants, and scattered accent use.
- Tokens: warm the neutral ramp, add spacing/radius/type/motion scales,
--accent-quiet + panel highlight. Terracotta hue unchanged.
- Primitives: .panel, .section-label, .page-header/.toolbar — collapse
the copied surfaces and label variants onto shared classes.
- Unify each filter-heavy page's display controls (group/sort/view,
select-all, count) into a workspace toolbar inside the filter panel,
so they read as one surface instead of floating above it.
- Labels: sentence-case, higher-contrast form labels; uppercase section
markers bumped faint -> muted.
- Accent hierarchy: reserve full terracotta for primary actions/active
state; demote decorative labels and the group-header underline.
- Responsive groundwork: reflow classes for the deck two-pane and nav
rail (stacks / icon-collapse at breakpoints), touch-target sizing.
Desktop unchanged in kind.
2026-07-22 23:44:21 -05:00
|
|
|
|
</div>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{rows.length === 0 ? (
|
|
|
|
|
|
<p className="hint">
|
|
|
|
|
|
No matching cards owned. Add some in the Add Cards tab, or loosen the filters.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
groups.map((g) => (
|
|
|
|
|
|
<div key={g.label || "all"} style={{ marginBottom: g.label ? 20 : 0 }}>
|
|
|
|
|
|
{g.label && (
|
2026-07-20 00:21:22 -05:00
|
|
|
|
<h3 className="group-header">
|
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
|
|
|
|
{g.label} <span className="badge">{g.cards.length}</span>
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{view === "list" ? libTable(g.cards) : libTiles(g.cards)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-10-17 21:58:44 -05:00
|
|
|
|
|
|
|
|
|
|
/** A picked file's name without its folder or extension — a ManaBox export
|
|
|
|
|
|
* is named after its binder ("Green Bulk.csv"). Copes with Android content
|
|
|
|
|
|
* URIs, whose last segment is a percent-encoded "primary:Download/x.csv". */
|
|
|
|
|
|
function fileStem(path: string): string {
|
|
|
|
|
|
let name = path;
|
|
|
|
|
|
try {
|
|
|
|
|
|
name = decodeURIComponent(path);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Not percent-encoded after all; use it as it is.
|
|
|
|
|
|
}
|
|
|
|
|
|
name = name.split(/[/\\:]/).pop() ?? "";
|
|
|
|
|
|
return name.replace(/\.[^.]*$/, "").trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Asks where an import's cards are kept, pre-filled with the file's name.
|
|
|
|
|
|
* Blank imports them without touching any card's location. */
|
|
|
|
|
|
function ImportLocationDialog({
|
|
|
|
|
|
path,
|
|
|
|
|
|
onImport,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
path: string;
|
|
|
|
|
|
onImport: (location: string) => void;
|
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [value, setValue] = useState(() => fileStem(path));
|
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
useEffect(() => inputRef.current?.select(), []);
|
|
|
|
|
|
const submit = () => onImport(value.trim());
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DialogFrame title="Where are these cards?" onCancel={onCancel}>
|
|
|
|
|
|
<label className="dialog-field">
|
|
|
|
|
|
<span className="muted">Location</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
placeholder="e.g. Box 1"
|
|
|
|
|
|
onChange={(e) => setValue(e.target.value)}
|
|
|
|
|
|
onKeyDown={(e) => e.key === "Enter" && submit()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p className="dialog-message">
|
|
|
|
|
|
Every imported card is filed under this location, replacing any it already had. Leave it
|
|
|
|
|
|
blank to keep cards' current locations.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="dialog-actions">
|
|
|
|
|
|
<button onClick={onCancel}>Cancel</button>
|
|
|
|
|
|
<button className="primary" onClick={submit}>
|
|
|
|
|
|
Import
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</DialogFrame>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|