It's an implementation detail the app deliberately hides: the user names and moves a JSON file, and nothing else. Documenting the derived SQLite copy invited people to think about (or back up, or move) a file that is rebuilt on demand. The rationale for keeping it in private storage stays where it belongs, in sync.rs's module docs.
274 lines
11 KiB
Markdown
274 lines
11 KiB
Markdown
# 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
|
|
```
|
|
|
|
Two files:
|
|
|
|
- **your library `.json`** — owned cards, decks, deck contents. Small,
|
|
readable, and the only file you name or move. Put it somewhere synced; see
|
|
[Sync](#sync).
|
|
- **`reference.db`** — read-only card data built from Scryfall bulk. Bulky and
|
|
re-downloaded per device, so it's never synced. Lives in the OS app-data dir
|
|
(e.g. `~/.local/share/com.garrettmills.mtgc/`).
|
|
|
|
## 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/<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.
|
|
|
|
### 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.
|
|
|
|
## 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
|
|
```
|