diff --git a/README.md b/README.md index a4404fc..2bcb63e 100644 --- a/README.md +++ b/README.md @@ -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