Garrett Mills 94e4f25ec4 Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"Download card data" had been failing on every platform with "missing field
`download_uri`". Scryfall changed the bulk-data API: entries now carry
`jsonl_download_uri`/`compressed_size` instead of `download_uri`/`size`, and
the payload is gzipped JSONL (one card object per line) rather than an
uncompressed JSON array. An existing reference.db keeps working, which is why
this went unnoticed.

- Rename the BulkEntry fields and decompress with flate2 (the response is
  Content-Type: application/gzip with no Content-Encoding, so ureq won't
  transparently inflate it). rust_backend keeps it pure Rust, so nothing new
  has to cross-compile for Android.
- Replace the streaming JSON-array visitor with a line-based JSONL parser.
  Closure errors now propagate directly instead of being laundered through
  serde::de::Error::custom, so cancellation reports a clean Cancelled;
  override_if_cancelled stays as a safety net and its doc comment is corrected.
- Progress wraps the compressed stream, since compressed_size is what the index
  reports.
- Test fixtures stay readable pretty-printed JSON arrays via a jsonl() helper
  that converts them, so tests still exercise the production parser.

Downloads are far smaller now: 101MB compressed (24 + 77) versus ~740MB, which
matters a lot on a phone.

Also log command failures to the platform log. Tauri command errors are
returned to the WebView as values, so they never reached logcat and the UI only
showed them truncated -- this bug was invisible until then. mlog! is a plain
eprintln!, which tao's Android glue forwards to logcat under RustStdoutStderr,
so no logging dependency is needed. View with: adb logcat -s RustStdoutStderr

Verified against the live API: 36,242 oracle cards, 116,409 printings, trigram
FTS working, and the aarch64 APK still builds.
2026-08-02 14:00:04 -05:00
2026-08-02 13:16:34 -05:00
2026-08-02 13:16:34 -05:00
2026-08-02 13:16:34 -05:00

mtgc — Personal Magic: The Gathering Library Catalog

A desktop app (Tauri v2 + React) for cataloging your MTG collection: rapid fuzzy card entry, rich criteria search (colors, commander color-identity compatibility, mana value, type/subtype/keyword), and deck organization with per-card deck membership. Built so the eventual mobile port is cheap — all logic lives in a platform-agnostic Rust core.

Architecture

src/                 React + TypeScript UI (swappable presentation)
src-tauri/           Tauri v2 shell — exposes the core as typed commands
crates/mtgc-core/    Pure Rust: db, fuzzy search, filters, decks, ingester

Two SQLite databases, joined at runtime via ATTACH:

  • reference.db — read-only card data built from Scryfall bulk. Bulky, re-downloadable per device, not synced.
  • library.db — your owned cards + decks. Small, portable, the file you sync (drop it in Syncthing/Dropbox/Nextcloud). Point the app at it via Choose library file….

Both default to the OS app-data dir (e.g. ~/.local/share/com.garrettmills.mtgc/).

First-time setup

./scripts/devel-setup.sh     # Tauri system libs (webkit2gtk/gtk3) + Tauri CLI
npm install                  # frontend deps

Why GTK3, not GTK4? Tauri v2's Linux backend (wry/tao) hard-depends on GTK3 + webkit2gtk-4.1; there's no GTK4/webkitgtk-6.0 option yet. GTK4 support is tracked upstream but unmerged with no ETA: tauri#7335, tao#1104, wry#1228. Revisit once Tauri ships GTK4 support (likely Tauri v3).

Run

cargo tauri dev              # launches the desktop app (dev mode)

On first launch, click Download card data — it fetches the Scryfall Oracle Cards + Default Cards bulk (~a few hundred MB) and builds reference.db. After that the app is fully offline.

You can also build the card database from the command line:

cargo run -p mtgc-core --bin mtgc-ingest -- /path/to/reference.db

Production build

Build an optimized app with the frontend embedded into the executable:

./scripts/devel-setup.sh   # once, if not already done
cargo tauri build

Outputs (under the workspace target/release/):

  • target/release/mtgc — the standalone executable. The web UI and all app logic are baked into this one file. It dynamically links the system WebKit/GTK libraries (webkit2gtk4.1, gtk3), so those must be present on the machine that runs it — they are on any modern desktop Linux and are installed by devel-setup.sh.
  • target/release/bundle/rpm/*.rpm and .../deb/*.deb — native installers (Fedora / Debian-based) that add a menu entry and icon.

Useful variants:

cargo tauri build --no-bundle          # only the bare target/release/mtgc binary

Notes:

  • The mtgc binary is a single file but relies on the system WebKit/GTK libs being present (installed by devel-setup.sh).
  • The binary does not embed card data — first run still downloads the Scryfall bulk into the OS app-data dir, and your synced library.db lives there by default (override with Choose library file…).
  • The [profile.release] settings in Cargo.toml (strip + LTO) make the release build take a few minutes but keep the binary small.

Using it

  • Add Cards — type a name (typos OK); ↑/↓ then Enter adds a copy.
  • Library — filter owned cards by name, colors, commander identity (color identity ⊆ chosen), mana value range, type/subtype/keyword, or deck. Hover a row's thumbnail for a full-size preview that tracks the cursor. Rows show which decks a card is in and a warning (⚠) when it's assigned to more decks than you own. Toggle List / Tiles (top-right) to switch between the table and a grid of full card images; the choice is remembered.
  • Deck contents offer the same List / Tiles toggle and hover previews.
  • Decks — create decks and assign owned cards; set per-deck quantities.

Develop / test

cargo test -p mtgc-core      # core unit tests (db, search, filters, decks, ingest)
npm run build                # typecheck + build frontend
cargo build                  # build everything incl. the desktop app

Android

The Rust core and frontend are platform-agnostic, so the Android build shares all app logic with the desktop; only data provisioning differs.

One-time setup

Install Android Studio and launch it once so its wizard downloads the SDK, then:

./scripts/android-setup.sh

That adds what Android Studio doesn't install by default: the SDK command-line tools, a pinned stable NDK, and the Rust Android targets. It prints the three environment variables to export (ANDROID_HOME, NDK_HOME, JAVA_HOME).

Why the NDK? mtgc-core builds rusqlite with the bundled feature (compiles SQLite from C) and rustls pulls in ring (C/asm), so a C cross-toolchain is required. The bundled SQLite can't be swapped for Android's system libsqlite3 either — the card schema needs FTS5 with the trigram tokenizer, which platform SQLite often lacks.

Why JAVA_HOME? Gradle/AGP reject very new JDKs. The script prefers Android Studio's bundled JetBrains Runtime (JDK 21), which needs no extra install.

Build and run

Every Android command needs ANDROID_HOME / NDK_HOME / JAVA_HOME. Source the helper once per shell — it auto-detects all three:

source scripts/android-env.sh      # bash / zsh
source scripts/android-env.fish    # fish

Only the env helper is shell-specific (it has to mutate the calling shell); android-setup.sh is executed rather than sourced, so it works from any shell.

Then, from the project root:

cargo tauri android dev --open   # dev server + open in Android Studio
cargo tauri android build --apk  # standalone release APK

src-tauri/gen/android is committed (unlike the rest of gen/) because AndroidManifest.xml is hand-edited — Android permissions have no tauri.conf.json equivalent.

Working in Android Studio

Use cargo tauri android dev --open rather than opening src-tauri/gen/android directly. Studio's Run button delegates the Rust build to cargo tauri android android-studio-script, which expects a running Tauri dev session — it reads the dev-server address from /tmp/<identifier>-server-addr. Open the Gradle project on its own and the build fails with failed to read missing addr file.

With the dev session running, Studio's device dropdown, Device Manager (emulator), and debugger all work normally.

JetBrains Toolbox users: --open finds Studio by scanning XDG data dirs for a .desktop entry whose Name is exactly Android Studio. Toolbox writes a versioned name instead (e.g. Android Studio Quail 2 2026.1.2 Patch 1), so the lookup misses and the launch fails with a confusing Cmd(["Android Studio"]) ... No such file or directory. android-setup.sh drops in a hidden shim entry (~/.local/share/applications/android-studio.desktop) to fix this; delete that file to undo it.

To build without Studio and without a dev server, use cargo tauri android build — that path needs no addr file.

Card data on a phone

Downloading card data streams ~740MB of JSON to build a ~73MB database, which is slow on mobile and may be killed if the app is backgrounded. Faster: copy reference.db from a desktop install onto the device and use Import card data…. The file is validated before it replaces anything.

Branding

The logo is an original pirate-ship silhouette in the terracotta brand color (#C8553D) — a nod to the MTG card "Pirate Ship", not a reproduction of its art. Source: src-tauri/icons/logo-source.svg. To regenerate the full platform icon set after editing it:

cargo tauri icon src-tauri/icons/logo-source.svg
Description
No description provided
Readme 4.9 MiB
Languages
Rust 57.5%
TypeScript 29.4%
CSS 7.9%
Shell 4.4%
Kotlin 0.6%