Update README for document-based sync and the Android port

The data-model section still described library.db as "the file you sync",
which is no longer how any of this works. Replace it with the document +
internal working copy split, and add a Sync section covering the three-way
merge, when sync runs, and the one-time conversion from an older library.db.

Also: the app is no longer desktop-with-a-future-mobile-port, so say so;
document the SAF folder grant on Android and why no storage permission is
declared; list the filters added since this was last touched, including the
caveat that the reference.db-backed ones need one card-data rebuild; and
mention the shell test suite and the Kotlin-only typecheck.
This commit is contained in:
2026-08-07 22:08:35 -05:00
parent 04e9c3cbbb
commit 96489f8e7c

105
README.md
View File

@@ -1,10 +1,10 @@
# 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.
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
@@ -14,15 +14,18 @@ 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`:
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-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…*.
re-downloaded per device, **never synced**.
Both default to the OS app-data dir (e.g. `~/.local/share/com.garrettmills.mtgc/`).
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
@@ -85,8 +88,8 @@ 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…*).
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.
@@ -94,26 +97,80 @@ Notes:
- **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
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 unit tests (db, search, filters, decks, ingest)
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 differs.
all app logic with the desktop; only data provisioning and file access differ.
### One-time setup
@@ -196,6 +253,22 @@ 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