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,
|
|
|
|
|
|
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,
|
|
|
|
|
|
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-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,
|
|
|
|
|
|
groupCards,
|
2026-07-22 20:55:17 -05:00
|
|
|
|
sortCards,
|
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";
|
2026-10-23 22:46:45 -05:00
|
|
|
|
import { CardFilterPanel, NO_FACETS, useCardFilter } from "../cardFilter";
|
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
|
|
|
|
// 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-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;
|
|
|
|
|
|
}) {
|
2026-10-23 22:46:45 -05:00
|
|
|
|
const filterState = useCardFilter("library-criteria");
|
|
|
|
|
|
const { filter, criteria } = filterState;
|
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 t = setTimeout(() => {
|
|
|
|
|
|
api.searchLibrary(filter).then(setRows).catch(() => setRows([]));
|
|
|
|
|
|
}, 120);
|
|
|
|
|
|
return () => clearTimeout(t);
|
2026-10-23 22:46:45 -05:00
|
|
|
|
}, [filter, 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">
|
2026-10-23 22:46:45 -05:00
|
|
|
|
<CardFilterPanel state={filterState} facets={facets} decks={decks} hideNotOwned />
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|