The list now lives for the app session (per library) instead of resetting
whenever the tab is left. Each row gets a button opening the card details
modal to set copies' printings, plus the deck's printing and cover when
the card was sent to a deck; owned-count changes there update the row.
Closes#28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deck picker beside the location field adds each card to the library and
then to the chosen deck. The copy fills a slot the deck is short on
(unowned or claimed elsewhere for idea decks, not yet pulled for
building/built decks), otherwise the deck gets one more copy. A
maybeboarded card moves to the main board, and building/built decks count
the copy as pulled instead of filing it under a location. Retired decks
aren't offered.
Closes#16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Columns menu next to the List/Tiles toggle hides or shows optional
columns in the Library, Card Search, deck builder and Decks list row
views. Choices are saved per view on each device. The selection checkbox
and name always show.
Closes#8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filters by set code, collector number, year, rarity, language and finish,
matching each word against the start of a field. Shown when a card has 7+
printings.
Closes#31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cards like Relentless Rats (any number), Nazgûl (up to nine), and Once
More with Feeling (only one) override the format's copy limit. The limit
is parsed from oracle text at card-data import into a new
oracle_cards.copy_limit column; older reference dbs fall back to the
format rule until re-imported. The legality check and illegal-card
selection use it. Also count snow basics as basic lands.
Closes#27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only building and built decks now claim owned copies, so several idea
decks can share one copy without an over-assignment warning. Deck cards
track how many copies are physically pulled: physical decks list cards
to pull with their source (a storage box, or another deck to take a copy
from), and idea decks split cards into available / in other decks / not
owned. The deck list shows status chips with to-pull counts, filters by
status, and collapses retired decks. Status and pulled counts sync,
which bumps the library document to version 4.
Existing decks migrate as ideas.
Closes#25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legality issues now carry a category (deck size, commander, color identity,
too many copies, banned/restricted) and the offending card. The legality
tile groups them and offers per-category and all-issue selection; over-limit
copies select only the extras, so bulk remove trims to the legal count.
Closes#26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes#9Closes#11Closes#12
A share button beside the preview's close button sends the face being
shown at Scryfall's large size: on desktop the image is decoded and put on
the clipboard (tauri-plugin-clipboard-manager); on Android it's saved to
the cache and handed to the system share sheet by a small in-repo plugin
(SharePlugin.kt) through the app's FileProvider. The preview's corner
buttons are also a little larger.
An About panel shows the installed version (from Tauri at runtime, since
release builds stamp it at build time; dev builds are labelled as such).
On wide screens the page splits into panels in two columns — card
database, library stats and libraries on the left; sync and about on the
right — while narrow screens keep the single-column order. The sync panel
is now headed "Sync" and the Info page's summary "Library stats".
Closes#22
A "cost" tile totals the USD price of every copy in the deck (commander
included, maybeboard not). The stat tiles now span the panel and size to
their values, with the color bars and mana curve sharing the row below,
and the left-hand panels no longer spread out to match the analysis
panel's height.
Closes#20
Deck-name chips in the Library, Card Search and the card preview modal are
now buttons (DeckChip) that open that deck in the Decks tab — on mobile,
straight to its building screen. App provides the open-deck callback via
context and hands the deck id to DeckView.
Closes#19
A fold() (lowercase + strip accents, using the same table as nucleo's own
normalization) is registered as a SQLite function and used by the name,
oracle-text and location filters, so "khazad-dum" finds Troll of
Khazad-dûm. The fuzzy name search folds the query too (accented queries
used to match nothing) and its exact-name promotion; decklist/ManaBox
import falls back to a folded name match; the deck quick filter and tag
suggestions fold client-side.
Closes#17
Cards with no owned copies get an "Add to library" button (tile corner and
list row) that records the deck's copy count as owned, and the selection
bar gains a bulk "Add to library" that adds just the missing copies.
Closes#10
Extract the filter fields and LibraryFilter-building state into
src/cardFilter.tsx (CardFilterPanel + useCardFilter) and use it in the
Library, Card Search and News set browser, replacing three diverging
copies. search_reference now honors in_deck/location too, so every
filter works in every tab.
Closes#13
The preview's Owned row gets the same −/+ stepper as the library list,
refetching copy groups (which shrink with the total) and refreshing the
caller. Card Search now passes a preview context too, so its previews get
the stepper and printing controls.
Closes#3
Remove-selected went through the single-card stepper path, so every card
lingered dimmed for the 5-second undo grace period. A deliberate bulk
removal now deletes and drops the rows at once.
A Commander/Brawl deck can now have two commanders when their abilities
allow it. Partner abilities are parsed from oracle text (Scryfall's keyword
list files every "Partner—Survivors"-style variant under plain "Partner"),
and a second search box offers only cards that can pair with the commander.
The legality check accepts a valid pair and checks color identity against
both; suggestions and decklist import/export handle both commanders.
Closes#4
On Android the open dialog returns a content:// URI, which std::fs can't
open, so ManaBox/library and deck imports failed with "no such file or
directory". Read them via tauri-plugin-fs, which resolves content URIs and
falls through to plain paths on desktop.
Closes#24
After picking a file, Import cards… asks where the cards are kept,
pre-filled with the file's name (a ManaBox export is named after its
binder). A location is set on every imported card, replacing any it had,
as Add Cards does; blank leaves locations alone. It's applied inside the
import's single transaction.
Android Studio now bundles JBR 25, which Gradle 8.14's embedded Kotlin
can't parse. Follow Tauri's current template: Gradle 9.6.1, Kotlin 2.2.10,
the builtInKotlin/newDsl/failOnMissingFiles compatibility flags, the
compilerOptions DSL, and ExecOperations in BuildTask (project.exec is gone
in Gradle 9). AGP goes to 9.4.1 rather than the template's 9.3.1.
Release lint vital is turned off: AGP 9.3/9.4's lint crashes analysing the
.gradle.kts scripts (findFirCompiledSymbol), failing every release build.
The env scripts now accept JDK 17 through 25, preferring Studio's JBR.
Check each JDK candidate's major version instead of assuming Android Studio's
JBR is 21, look for Adoptium temurin-{21,17}-jdk (Fedora 44 dropped
java-21-openjdk), and propagate android-env.sh failures to scripts that
source it so fdroid-publish.sh stops before building.
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.
The app can keep any number of named libraries and switch between them
from the top bar or Info → Libraries. Each can sync to the account's other
devices or stay on this one (a scratch library to try things in).
The sync server now keeps several libraries per user under
/api/libraries; a migration makes each existing library the user's default
"Main", which /api/library keeps serving for older app builds. Every sync
lines the device's libraries up with the account's: new ones elsewhere are
downloaded, new ones here created on the server, ones deleted elsewhere
flagged rather than thrown away. The existing library keeps its files and
merge base, so upgrading asks nothing.
Adjusts the tile grid's column count as an offset from the existing
width-based auto-fill default, shared across Library, Decks, Card search,
and set card lists. Clicking the count resets to the default.
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.
scripts/windows-publish.sh builds the NSIS setup.exe for
x86_64-pc-windows-msvc through cargo-xwin, versioned like the RPM and
APK, and copies it into a folder to serve: the newest three installers,
a stable mtgc-latest-x64-setup.exe, and SHA256SUMS.
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.
Same as the deck screen's own back button. Only claimed below the 720px
.deck-layout breakpoint, where the list and the builder are separate
screens; side by side, flipping mobileShowList changes nothing visible.
Adds useBackButton, a stack of back-button claimants (last claimed wins)
backed by Tauri's app-plugin back-button event. The listener is only
registered while something has claimed the button, so at the root back
keeps its native leave-the-app behavior.
Claimed by: the click-to-preview modal, DialogFrame (so Suggest cards,
Sample hand and every other dialog), and NewsView while a set's card
list is open. A preview opened from inside a dialog closes first.
- Toolbar (pane toggle + close) moves below the pane for thumb reach.
- The compact modal gets a fixed box sized to its largest pane, with each
pane anchored just above the toolbar, so switching to the (shorter)
info tile no longer moves the buttons. A too-tall info tile scrolls
within itself.
- Horizontal (Battle) art now uses the same post-toolbar height budget as
portrait art, computed once in JS from the measured available height.
- Fix useRotatedPreviewBox's width/height bounds being swapped, which let
rotated art overflow its width cap (e.g. ~469px wide on a 375px phone).
- Only apply the narrow side-by-side DFC budget at the >=860px breakpoint
where the faces actually sit side by side; below it a DFC Battle's front
was rendered at ~43vw.
When the full layout (card art + info tile) would be taller than the
viewport, typically on phones where the tile wraps below the art, the
modal switches to a compact mode: a toolbar with a Front/Back/Info (or
Card/Info) toggle and the close button, showing one pane at a time with
the art shrunk to fit the remaining height. Decided by measuring the full
layout on mount and after each resize; the art is now sized explicitly
from its known 488x680 aspect so that measurement doesn't depend on the
image having loaded.
Flex-centering in a scrolling container overflows off both edges when the
card art + info tile is taller than the viewport, and the part pushed above
the top can't be scrolled to. Center with margin: auto instead.
scripts/rpm-publish.sh builds the RPM, signs it and the repo metadata
with a dedicated GPG key, and adds it to a dnf repo in a given directory,
keeping the newest three versions for downgrades. The directory holds
nothing secret, so it can be served as is.
Both publish scripts now share the version scheme in
scripts/lib/release-version.sh: <major>.<minor>.<commit count>, plain
enough for RPM, MSI and semver alike. Android's versionCode stays the
commit count.
Fixes the publish scripts failing under set -u when the repo URL
variable isn't set, which is every run after the first. tauri.conf.json
now declares the license and homepage for the package metadata.
fdroidserver refuses a repo_url that doesn't end in /repo, but only at
`fdroid update`, after the APK was already copied in. The leftover APK
then made every retry think that version was published. Check the URL
up front, and take the APK back out if the update fails.
FDROID_REPO_URL now also updates repo_url in an existing repo, instead
of being read only when the repo is created.
The image now runs as USER 10001:10001 with a matching group, so
Kubernetes can verify runAsNonRoot and fsGroup: 10001 grants it a mounted
volume. When the database can't be opened because its directory isn't
writable, the server says so and how to fix it, instead of SQLite's bare
"unable to open database file". The README covers the pod securityContext.
scripts/fdroid-publish.sh builds a signed arm64 APK and adds it to an
F-Droid repo in a given directory, creating the app signing key and the
repo on first use. The versionCode is the commit count, so any commit can
ship without a manual version bump.
Release builds are signed when src-tauri/gen/android/keystore.properties
exists and stay unsigned otherwise. The manifest picks up the deep-link
plugin's intent filter, which every build regenerates and was never
committed.
Drop the Compose/Caddy example: production runs on Kubernetes, and locally
the container only needs a forwarded port and a volume. The README now gives
the `docker run` steps and what a Kubernetes deployment needs (one replica,
since it's a single SQLite file; a volume at /data; TLS at the ingress).
The server only shut down on Ctrl-C, so `docker stop` and pod termination,
which send SIGTERM, waited out the grace period and killed it. It now shuts
down gracefully on either.
Also ignore `.env`, which can hold the OIDC client secret.
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
"Not owned" moves into the shared Quick filters icon-toggle row (hidden on
the Library tab, where it'd always be a no-op since that view is owned-only
already) instead of a standalone button.
Tiles view gains a "Horizontal" toggle that rotates actually-horizontal
cards (Battles, Planes, Phenomena) into their natural landscape orientation
via a container-query-sized wrapper, spanning two grid columns so the
rotated card isn't squeezed into a single portrait column's width and left
too small to read.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Filtering for creatures with an empty rules box can't be done from
`oracle_cards.oracle_text`: that column joins every face together, so an
Adventure card always carries its spell half's text and a naive `oracle_text
= ''` test would exclude exactly the cards worth including.
Derive it at ingest instead, from the front face's own text — the front is the
half that resolves as the creature, and adventure/prepared/transform cards are
all cast from it. Exposed as `vanilla_only` behind its own `has_vanilla`
capability check rather than folded into `has_universes_beyond`, since it
lands in a later reference-schema revision and a db can have one column
without the other. Needs a card-data re-download before it does anything.
Filtering by category alone wasn't granular enough: a deck usually matches
several archetypes and tribes at once, so "show me Archetype suggestions"
still returns most of the list.
SynergyReason now carries the matched thing itself (`subject`) alongside the
human-readable `detail`, populated at all four match sites. The modal adds a
second chip row listing every subject present in the current suggestions,
sorted by how many cards each explains and labelled with that count, so the
most useful narrowing is obvious before clicking.
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.