# mtgc — Personal Magic: The Gathering Library Catalog A desktop and Android 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, price, format legality), and deck organization with per-card deck membership. All logic lives in a platform-agnostic Rust core, so both platforms share it. ## 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 ``` One file you manage, and two you don't: - **your library `.json`** — owned cards, decks, deck contents. Small, readable, **the only file you name or move**. See [Sync](#sync). - **`working.db`** — internal SQLite working copy of that document. Rebuilt whenever needed; nothing is lost if you delete it. - **`reference.db`** — read-only card data built from Scryfall bulk. Bulky, re-downloaded per device, **never synced**. The two databases are joined at runtime via `ATTACH` and both live in the OS app-data dir (e.g. `~/.local/share/com.garrettmills.mtgc/`). The library document lives wherever you put it. ## First-time setup ```bash ./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](https://github.com/tauri-apps/tauri/issues/7335), > [tao#1104](https://github.com/tauri-apps/tao/pull/1104), > [wry#1228](https://github.com/tauri-apps/wry/issues/1228). Revisit once Tauri > ships GTK4 support (likely Tauri v3). ## Run ```bash 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: ```bash 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: ```bash ./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: ```bash 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. Your library document defaults to that directory too; move it into a synced folder with **New Library** (see [Sync](#sync)). - 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, power/toughness, price, format legality, 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. - **Card Search** — the same filters over *every* card, owned or not. Select rows (or tiles) and bulk-add them to a deck; one undo covers the whole batch. - Deck contents offer the same List / Tiles toggle and hover previews. - **Decks** — create decks and assign owned cards; set per-deck quantities. Both filter panels share a row of quick toggles: hide **Universes Beyond** (cards that exist only as another property — Lord of the Rings, Warhammer 40,000, Marvel), hide **digital-only** (Arena/MTGO cards such as Alchemy rebalances), hide **Un-set** joke cards, or keep only cards that **can be a commander**. The three exclusions are derived as *every* printing, so an ordinary card reprinted into one of those sets — Lightning Bolt appears in Marvel Commander — isn't hidden. > Those three, plus the power/toughness filters, live in `reference.db`, which > is rebuilt rather than migrated. After upgrading, click **Update card data** > once to enable them; until then they're inert rather than broken. Price, > format legality and *can be a commander* work immediately. ## Sync Your collection is a single JSON file — the only file you manage. Put it in a folder your sync tool watches (Syncthing, Dropbox, Nextcloud, Seafile, or a USB stick) and every device stays in step. Choose it with **Open Library** / **New Library**, or from the **Info** tab. Editing in two places merges rather than one side overwriting the other. Sync is a three-way merge against the last version of the document that device read, which is what lets it tell "you changed this" from "they deleted it": edit different cards on a phone offline and on the desktop, and both survive. When both sides change the *same* field, the other copy's value wins and the change is reported so you can correct it. It runs by itself — at launch, on every switch away from and back to the app, and every few minutes while open. **Sync now** in the Info tab forces a pass. Nothing here depends on a particular sync tool. If two devices publish while both are offline, some tools leave a conflict copy behind; mtgc merges any it finds beside the document and clears it once the contents are safely published. > **Upgrading from an older build?** Earlier versions had you manage > `library.db` yourself. The Info tab offers **Convert old library** once; the > original file is left untouched, so it doubles as a backup. ## Develop / test ```bash cargo test -p mtgc-core # core: db, search, filters, decks, ingest, sync merge cargo test -p mtgc # shell: document read/write, conflict-copy matching npm run build # typecheck + build frontend cargo build # build everything incl. the desktop app ``` The sync merge is a pure function over plain structs (`mtgc_core::sync::merge3`), so the cases that matter — concurrent edits, a delete racing an edit, repeated merges being a no-op — are unit tests rather than something you have to reproduce across two devices. To typecheck the Android Kotlin without building the APK (it doesn't invoke cargo, so it won't disturb a running dev session): ```bash cd src-tauri/gen/android && ./gradlew :app:compileUniversalDebugKotlin ``` ## Android The Rust core and frontend are platform-agnostic, so the Android build shares all app logic with the desktop; only data provisioning and file access differ. ### One-time setup Install Android Studio and **launch it once** so its wizard downloads the SDK, then: ```bash ./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: ```bash source scripts/android-env.sh # bash / zsh ``` ```fish 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: ```bash 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/-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. ### Sync on Android Android can't hand out a filesystem path to a shared folder, so instead of naming a file you grant access to a *folder* through the system picker: **Info → Choose folder…**, then pick the one your sync tool watches. mtgc keeps `library.json` inside it and everything in [Sync](#sync) applies unchanged. That grant is per-folder and persists across restarts, so the app declares **no storage permission at all** — no "All files access" prompt. The alternative, `MANAGE_EXTERNAL_STORAGE`, would hand the app the entire shared volume just to reach one directory. The working database stays in app-private storage regardless: SQLite can't open a `content://` URI, and a database on a sync-managed filesystem risks torn writes. Only the document crosses that boundary. ## 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: ```bash cargo tauri icon src-tauri/icons/logo-source.svg ```