Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
# mtgc — Personal Magic: The Gathering Library Catalog
|
|
|
|
|
|
2026-08-07 22:08:35 -05:00
|
|
|
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.
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
src/ React + TypeScript UI (swappable presentation)
|
|
|
|
|
src-tauri/ Tauri v2 shell — exposes the core as typed commands
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
crates/mtgc-core/ Pure Rust: db, fuzzy search, filters, decks, ingester, sync
|
|
|
|
|
crates/mtgc-server/ Optional sync server: one library per user, OIDC and/or passwords
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
```
|
|
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
Two kinds of data, both in the OS app-data dir (e.g.
|
|
|
|
|
`~/.local/share/com.garrettmills.mtgc/`):
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
- **your library** — owned cards, decks, deck contents. Lives on each device
|
|
|
|
|
and works fully offline; sign in to a sync server to share it between
|
|
|
|
|
devices (see [Sync](#sync)). **Export library…** in the Info tab saves it as
|
|
|
|
|
a JSON backup.
|
2026-08-07 22:10:38 -05:00
|
|
|
- **`reference.db`** — read-only card data built from Scryfall bulk. Bulky and
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
re-downloaded per device, so it's never synced.
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
## First-time setup
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./scripts/devel-setup.sh # Tauri system libs (webkit2gtk/gtk3) + Tauri CLI
|
|
|
|
|
npm install # frontend deps
|
|
|
|
|
```
|
|
|
|
|
|
2026-07-07 19:49:50 -05:00
|
|
|
> **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).
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
## 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/`):
|
|
|
|
|
|
Rebrand to mtgc: pirate-ship logo, lowercase app name, mtgc binary
- New logomark: an original pirate-ship silhouette (hull, twin billowing
sails, swallow-tail pennant) in the terracotta brand gradient on a dark
square — a nod to the MTG card "Pirate Ship", not a reproduction of its
art. Source at src-tauri/icons/logo-source.svg; regenerate the full
platform icon set (desktop/Windows Store/iOS/Android) with
`cargo tauri icon src-tauri/icons/logo-source.svg`.
- Apply it in-app via a new inline ShipLogo component (App.tsx), replacing
the old "M" sidebar mark and "✦" empty-state mark with dark rounded
squares containing the ship glyph.
- Rename the app to lowercase "mtgc" everywhere: window title and
productName (tauri.conf.json), <title> (index.html), sidebar brand text,
empty-state heading.
- Rename the Tauri package/binary mtgc-app -> mtgc (and its lib crate
mtgc_app_lib -> mtgc_lib), so `cargo tauri build` now produces
target/release/mtgc; updated main.rs and every README reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:53:36 -05:00
|
|
|
- **`target/release/mtgc`** — the standalone executable. The web UI and all
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
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
|
Rebrand to mtgc: pirate-ship logo, lowercase app name, mtgc binary
- New logomark: an original pirate-ship silhouette (hull, twin billowing
sails, swallow-tail pennant) in the terracotta brand gradient on a dark
square — a nod to the MTG card "Pirate Ship", not a reproduction of its
art. Source at src-tauri/icons/logo-source.svg; regenerate the full
platform icon set (desktop/Windows Store/iOS/Android) with
`cargo tauri icon src-tauri/icons/logo-source.svg`.
- Apply it in-app via a new inline ShipLogo component (App.tsx), replacing
the old "M" sidebar mark and "✦" empty-state mark with dark rounded
squares containing the ship glyph.
- Rename the app to lowercase "mtgc" everywhere: window title and
productName (tauri.conf.json), <title> (index.html), sidebar brand text,
empty-state heading.
- Rename the Tauri package/binary mtgc-app -> mtgc (and its lib crate
mtgc_app_lib -> mtgc_lib), so `cargo tauri build` now produces
target/release/mtgc; updated main.rs and every README reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:53:36 -05:00
|
|
|
cargo tauri build --no-bundle # only the bare target/release/mtgc binary
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Notes:
|
|
|
|
|
|
Rebrand to mtgc: pirate-ship logo, lowercase app name, mtgc binary
- New logomark: an original pirate-ship silhouette (hull, twin billowing
sails, swallow-tail pennant) in the terracotta brand gradient on a dark
square — a nod to the MTG card "Pirate Ship", not a reproduction of its
art. Source at src-tauri/icons/logo-source.svg; regenerate the full
platform icon set (desktop/Windows Store/iOS/Android) with
`cargo tauri icon src-tauri/icons/logo-source.svg`.
- Apply it in-app via a new inline ShipLogo component (App.tsx), replacing
the old "M" sidebar mark and "✦" empty-state mark with dark rounded
squares containing the ship glyph.
- Rename the app to lowercase "mtgc" everywhere: window title and
productName (tauri.conf.json), <title> (index.html), sidebar brand text,
empty-state heading.
- Rename the Tauri package/binary mtgc-app -> mtgc (and its lib crate
mtgc_app_lib -> mtgc_lib), so `cargo tauri build` now produces
target/release/mtgc; updated main.rs and every README reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:53:36 -05:00
|
|
|
- The `mtgc` binary is a single file but relies on the system WebKit/GTK libs
|
2026-07-07 19:49:50 -05:00
|
|
|
being present (installed by `devel-setup.sh`).
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
- The binary does **not** embed card data — first run still downloads the Scryfall
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
bulk into the OS app-data dir. Your library lives there too; to share it
|
|
|
|
|
between devices, see [Sync](#sync).
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
- The `[profile.release]` settings in `Cargo.toml` (strip + LTO) make the release
|
|
|
|
|
build take a few minutes but keep the binary small.
|
|
|
|
|
|
2026-10-13 22:05:17 -05:00
|
|
|
### Distributing to Fedora (dnf repo)
|
|
|
|
|
|
|
|
|
|
Fedora machines get the app, and its updates, from a small dnf repo you host
|
|
|
|
|
yourself. `scripts/rpm-publish.sh` builds a signed RPM and adds it to the repo
|
|
|
|
|
in a directory you choose. It needs:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo dnf install rpm-sign createrepo_c
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The first run also makes the package signing key. Give it the address the
|
|
|
|
|
directory will be served at:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
RPM_REPO_URL=https://rpm.example.com/mtgc ./scripts/rpm-publish.sh --init-key ~/rpm-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After that, publishing is just:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./scripts/rpm-publish.sh ~/rpm-mtgc # or: npm run rpm:publish -- ~/rpm-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then, on each machine, add the repo once and install:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo dnf config-manager addrepo --from-repofile=https://rpm.example.com/mtgc/mtgc.repo
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo dnf install mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
From then on, `sudo dnf upgrade` picks up new releases.
|
|
|
|
|
|
|
|
|
|
- **Serve the whole directory.** Unlike the F-Droid repo, nothing in it is
|
|
|
|
|
secret. It holds the RPMs, the signed `repodata/`, the public key
|
|
|
|
|
(`RPM-GPG-KEY-mtgc`) and `mtgc.repo`. The newest three versions are kept, so
|
|
|
|
|
a bad release can be undone with `sudo dnf downgrade mtgc`.
|
|
|
|
|
- **Back up the signing key**, `~/.local/share/mtgc/gnupg`. It has no
|
|
|
|
|
passphrase, so publishing runs unattended. Without it, each machine has to
|
|
|
|
|
trust a new key before it takes another update.
|
|
|
|
|
- Versions work as on Android: `0.1.<commits on HEAD>`, see
|
|
|
|
|
[Distributing to phones](#distributing-to-phones-f-droid).
|
|
|
|
|
- The RPM is built against this machine's libraries, so it installs on this
|
|
|
|
|
Fedora release and newer.
|
|
|
|
|
|
2026-10-16 20:11:02 -05:00
|
|
|
### Distributing to Windows
|
|
|
|
|
|
|
|
|
|
The Windows installer is cross-built here on Linux and published to a plain
|
|
|
|
|
downloads folder you host. `scripts/windows-publish.sh` builds it through
|
|
|
|
|
[cargo-xwin](https://github.com/rust-cross/cargo-xwin) and needs, once:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo dnf install mingw-nsis-base llvm lld clang
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo install --locked cargo-xwin
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
rustup target add x86_64-pc-windows-msvc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then, for each release:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./scripts/windows-publish.sh ~/win-mtgc # or: npm run windows:publish -- ~/win-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
On Windows, download and run the setup.exe. A newer one upgrades the
|
|
|
|
|
installed app in place.
|
|
|
|
|
|
|
|
|
|
- **Serve the whole directory.** It holds the versioned installers
|
|
|
|
|
(`mtgc_0.1.<n>_x64-setup.exe`, the newest three), `mtgc-latest-x64-setup.exe`
|
|
|
|
|
as a stable link to the newest, and `SHA256SUMS`.
|
|
|
|
|
- Only the NSIS `setup.exe` can be built off Windows; an MSI needs WiX, which
|
|
|
|
|
runs only on Windows. The first build downloads the MSVC CRT and Windows SDK
|
|
|
|
|
into `~/.cache/cargo-xwin`.
|
|
|
|
|
- The installer isn't code-signed, so SmartScreen warns about an unknown
|
|
|
|
|
publisher the first time: **More info → Run anyway**. It fetches WebView2 if
|
|
|
|
|
the machine lacks it (Windows 10 and 11 normally ship it).
|
|
|
|
|
- Versions work as on Android: `0.1.<commits on HEAD>`, see
|
|
|
|
|
[Distributing to phones](#distributing-to-phones-f-droid).
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
## Using it
|
|
|
|
|
|
|
|
|
|
- **Add Cards** — type a name (typos OK); ↑/↓ then Enter adds a copy.
|
|
|
|
|
- **Library** — filter owned cards by name, colors, commander identity (color
|
2026-08-07 22:08:35 -05:00
|
|
|
identity ⊆ chosen), mana value, power/toughness, price, format legality,
|
|
|
|
|
type/subtype/keyword, or deck. Hover a
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
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.
|
2026-08-07 22:08:35 -05:00
|
|
|
- **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.
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
- Deck contents offer the same List / Tiles toggle and hover previews.
|
|
|
|
|
- **Decks** — create decks and assign owned cards; set per-deck quantities.
|
|
|
|
|
|
2026-08-07 22:08:35 -05:00
|
|
|
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.
|
|
|
|
|
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
### Printings
|
|
|
|
|
|
|
|
|
|
The library counts cards, not printings, but you can say which printing a copy
|
|
|
|
|
is when it matters. Click a card's image in the **Library** or a deck to open
|
|
|
|
|
its preview. The **Owned** section lists your copies by printing and finish
|
|
|
|
|
(nonfoil / foil / etched), with a stepper for each, and **Add a printing…**
|
|
|
|
|
picks another. Copies you haven't assigned a printing to stay *unspecified*:
|
|
|
|
|
"5 copies · 3 by printing · 2 unspecified". Assigning more copies to printings
|
|
|
|
|
than you own raises the owned count to match. Lowering the owned count drops
|
|
|
|
|
the most recently added printing entries first.
|
|
|
|
|
|
|
|
|
|
In a deck, **Shown in this deck → Change…** picks which printing's art that
|
|
|
|
|
slot shows, and **Default** goes back to the usual one. This is display only:
|
|
|
|
|
it doesn't need to be a copy you own.
|
|
|
|
|
|
|
|
|
|
Decklist export writes a chosen or tracked printing the way Moxfield and Arena
|
|
|
|
|
do, and import reads it back:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
1 Sol Ring (2X2) 315 *F*
|
|
|
|
|
4 Lightning Bolt (2XM) 129
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`*F*` marks foil and `*E*` etched. A library export writes one line per
|
|
|
|
|
printing, then a plain line for the unspecified copies. A suffix that doesn't
|
|
|
|
|
match any printing still imports the card itself.
|
|
|
|
|
|
|
|
|
|
> The picker's dates and languages need card data from this version. Until you
|
|
|
|
|
> update it, printings are listed without them. Whenever your card data is
|
|
|
|
|
> missing something the app could use, a notice under the top bar offers
|
|
|
|
|
> **Update card data**, and **Import card data…** too on a phone. Dismissing it
|
|
|
|
|
> with **Not now** lasts until a later version needs something more.
|
|
|
|
|
|
2026-08-07 22:08:35 -05:00
|
|
|
## Sync
|
|
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
Your library always lives on the device you're using, so the app works the
|
|
|
|
|
same with or without a network. To keep several devices in step, run an
|
|
|
|
|
[mtgc sync server](#running-the-sync-server) and sign in to it from each one:
|
|
|
|
|
**Info → Sync server**, enter its address, and **Sign in…**. Sign-in happens
|
|
|
|
|
in your browser, on the server's own page — with a username and password, or
|
|
|
|
|
through your single sign-on provider, whichever the server offers — and the
|
|
|
|
|
browser hands the app back a token for that device alone.
|
|
|
|
|
|
|
|
|
|
Editing in two places merges rather than one side overwriting the other. Each
|
|
|
|
|
device remembers the last version of the library it knows the server had, and
|
|
|
|
|
merges three ways against it — 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
|
|
|
|
|
server's value wins and the change is reported so you can correct it.
|
|
|
|
|
|
|
|
|
|
Sync runs by itself — at launch, on every switch away from and back to the app,
|
|
|
|
|
on reconnecting, and every few minutes while open. The top bar shows where it
|
|
|
|
|
stands (**Synced**, **Offline — changes will sync**, …); **Sync now** in the
|
|
|
|
|
Info tab forces a pass. Offline, edits simply wait on the device: nothing is
|
|
|
|
|
lost, and they go out with the next sync that reaches the server.
|
|
|
|
|
|
|
|
|
|
The first time a device signs in to an account that already has a library,
|
|
|
|
|
and the device has one too, it asks whether to **combine** them (nothing from
|
|
|
|
|
either is lost) or **use the server's library** instead of this device's.
|
|
|
|
|
|
|
|
|
|
The Info tab also lists the account's signed-in devices, so a lost phone can be
|
|
|
|
|
signed out from another one.
|
|
|
|
|
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
> **Printings and older versions.** A library with printings in it (see
|
|
|
|
|
> [Printings](#printings)) is saved in a newer format. A device or server
|
|
|
|
|
> running an older version refuses it and says to update, rather than syncing
|
|
|
|
|
> and quietly losing them. Libraries that don't use printings stay readable by
|
|
|
|
|
> older versions. Update the sync server before the first device starts using
|
|
|
|
|
> printings.
|
|
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
> **Upgrading from folder sync?** Earlier versions kept the library as a JSON
|
|
|
|
|
> file in a folder your sync tool watched. Your library is already on each
|
|
|
|
|
> device, so signing in to a server picks up from there; the first device to
|
|
|
|
|
> sign in uploads it. If one device's copy is out of date, use **Import library
|
|
|
|
|
> file…** on it to combine in the `library.json` from the synced folder. Builds
|
|
|
|
|
> older still had you manage `library.db` yourself; the Info tab offers
|
|
|
|
|
> **Import old library** once for that.
|
|
|
|
|
|
|
|
|
|
## Running the sync server
|
|
|
|
|
|
2026-10-08 23:09:34 -05:00
|
|
|
`mtgc-server` is a single binary with a SQLite database, shipped as a
|
|
|
|
|
container image. Inside the container it listens on port 8080 and keeps its
|
|
|
|
|
database in `/data`; everything else is environment variables (below).
|
|
|
|
|
|
|
|
|
|
To run it locally, build the image, then run it with the port forwarded to
|
|
|
|
|
localhost and a named volume for the data:
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
```bash
|
2026-10-08 23:09:34 -05:00
|
|
|
docker build -f crates/mtgc-server/Dockerfile -t mtgc-sync-server .
|
|
|
|
|
|
|
|
|
|
docker run -d --name mtgc-sync-server \
|
|
|
|
|
-p 127.0.0.1:8080:8080 \
|
|
|
|
|
-v mtgc-sync-data:/data \
|
|
|
|
|
-e MTGC_PUBLIC_URL=http://localhost:8080 \
|
|
|
|
|
mtgc-sync-server
|
|
|
|
|
|
|
|
|
|
docker exec -it mtgc-sync-server mtgc-server user add yourname # prompts for a password
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
```
|
|
|
|
|
|
2026-10-08 23:09:34 -05:00
|
|
|
Then sign in from the desktop app with `http://localhost:8080`. For more
|
|
|
|
|
settings, copy `crates/mtgc-server/deploy/.env.example` to `.env` beside it
|
|
|
|
|
(git ignores it) and pass `--env-file crates/mtgc-server/deploy/.env` instead
|
|
|
|
|
of the `-e`. Podman takes the same commands.
|
|
|
|
|
|
|
|
|
|
The container runs as UID 10001. A named volume, as above, picks up the right
|
|
|
|
|
ownership by itself; if you bind-mount a host directory instead, `chown 10001`
|
|
|
|
|
it first.
|
|
|
|
|
|
|
|
|
|
In production, put it behind something that terminates HTTPS — the Android
|
|
|
|
|
app refuses plain HTTP outside debug builds — and set `MTGC_PUBLIC_URL` to
|
|
|
|
|
the `https://` address users reach it at. On Kubernetes that means a
|
|
|
|
|
Deployment with **one replica** (it's a single SQLite file, so replicas can't
|
|
|
|
|
share it) and a `Recreate` strategy, a PersistentVolumeClaim mounted at
|
|
|
|
|
`/data`, a Service on port 8080, and an Ingress for TLS. `GET /healthz`
|
|
|
|
|
answers `ok` for probes. Keep `MTGC_OIDC_CLIENT_SECRET` in a Secret. Run the
|
|
|
|
|
account commands with `kubectl exec -it deploy/<name> -- mtgc-server user add
|
|
|
|
|
<name>`.
|
|
|
|
|
|
2026-10-13 21:21:36 -05:00
|
|
|
A freshly provisioned volume is usually owned by root, which the server
|
|
|
|
|
(UID/GID 10001) can't write to — it fails at startup with "`/data` isn't
|
|
|
|
|
writable". Have Kubernetes hand the volume to its group in the pod spec:
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
spec:
|
|
|
|
|
securityContext:
|
|
|
|
|
runAsNonRoot: true
|
|
|
|
|
runAsUser: 10001
|
|
|
|
|
runAsGroup: 10001
|
|
|
|
|
fsGroup: 10001
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A few volume types ignore `fsGroup` (`hostPath`, and some NFS and local-path
|
|
|
|
|
provisioners). For those, `chown 10001:10001` the backing directory once, or
|
|
|
|
|
add an init container that does it.
|
|
|
|
|
|
2026-10-16 20:12:11 -05:00
|
|
|
To build the image and push it to Docker Hub
|
|
|
|
|
([`glmdev/mtgc`](https://hub.docker.com/r/glmdev/mtgc)):
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
|
|
|
|
|
```bash
|
2026-10-16 20:12:11 -05:00
|
|
|
npm run sync-server:build # → docker.io/glmdev/mtgc:latest
|
|
|
|
|
npm run sync-server:push # after `docker login docker.io`
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Both call `scripts/sync-server-image.sh`. `IMAGE_TAG` overrides the `latest`
|
2026-10-16 20:12:11 -05:00
|
|
|
tag, `IMAGE_REPO` points it at another repository (e.g.
|
|
|
|
|
`IMAGE_REPO=registry.example.com/mtgc`), and `CONTAINER_ENGINE=podman` picks
|
|
|
|
|
podman when docker is also installed.
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
Configuration is all environment variables:
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
| Variable | Default | |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `MTGC_PUBLIC_URL` | *(required)* | The `https://` address users reach the server at, exactly as the proxy serves it |
|
|
|
|
|
| `MTGC_BIND` | `0.0.0.0:8080` | Listen address |
|
|
|
|
|
| `MTGC_DB` | `mtgc-server.db` | SQLite database path |
|
|
|
|
|
| `MTGC_LOCAL_AUTH` | `true` | Username/password accounts |
|
|
|
|
|
| `MTGC_OIDC_ISSUER` | — | Turns on single sign-on, e.g. `https://auth.example.com/application/o/mtgc/` |
|
|
|
|
|
| `MTGC_OIDC_CLIENT_ID`, `MTGC_OIDC_CLIENT_SECRET` | — | A confidential client, with redirect URI `${MTGC_PUBLIC_URL}/auth/callback` |
|
|
|
|
|
| `MTGC_OIDC_NAME` | `single sign-on` | Shown on the button: "Sign in with …" |
|
|
|
|
|
| `MTGC_OIDC_SCOPES` | `openid email profile` | |
|
|
|
|
|
| `MTGC_ALLOWED_EMAILS` | — | Comma-separated allow list of verified emails; unset leaves it to the provider |
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
With both sign-in methods on, the sign-in page offers both; with only single
|
|
|
|
|
sign-on, it goes straight to the provider. Each provider identity and each
|
|
|
|
|
local account is its own user with its own library.
|
|
|
|
|
|
|
|
|
|
Local accounts are managed from the command line; there's no sign-up page, so
|
|
|
|
|
a server on the internet can't be filled with accounts by strangers:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
mtgc-server user add <name> [--display-name <text>] # prompts for a password
|
|
|
|
|
mtgc-server user passwd <name>
|
|
|
|
|
mtgc-server user remove <name> --yes # also deletes their server-side library
|
|
|
|
|
mtgc-server user list
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The server keeps the last 50 versions of each library in its database
|
|
|
|
|
(`library_history`) as a safety net for recovering by hand.
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
## Develop / test
|
|
|
|
|
|
|
|
|
|
```bash
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
cargo test -p mtgc-core # core: db, search, filters, decks, ingest, sync merge + cycle
|
|
|
|
|
cargo test -p mtgc # shell: config, merge-base storage, sign-in listener
|
|
|
|
|
cargo test -p mtgc-server # server: sign-in, compare-and-swap, devices, end-to-end sync
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
npm run build # typecheck + build frontend
|
|
|
|
|
cargo build # build everything incl. the desktop app
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-07 22:08:35 -05:00
|
|
|
The sync merge is a pure function over plain structs (`mtgc_core::sync::merge3`),
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
and the sync cycle runs over a `Remote` trait (`mtgc_core::sync::remote`), so the
|
|
|
|
|
cases that matter — concurrent edits, a delete racing an edit, a lost reply, a
|
|
|
|
|
lost write race — are unit tests rather than something you have to reproduce
|
|
|
|
|
across two devices. `crates/mtgc-server/tests/sync_e2e.rs` then runs two
|
|
|
|
|
devices against the real server over HTTP.
|
|
|
|
|
|
|
|
|
|
To try sign-in by hand, run a server locally (`MTGC_PUBLIC_URL=http://127.0.0.1:8080
|
|
|
|
|
cargo run -p mtgc-server`, after `cargo run -p mtgc-server -- user add me`) and
|
|
|
|
|
sign in from the app with that address. From the Android emulator, the host is
|
|
|
|
|
`http://10.0.2.2:8080`; debug builds allow plain HTTP.
|
2026-08-07 22:08:35 -05:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
```
|
|
|
|
|
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
## Android
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
The Rust core and frontend are platform-agnostic, so the Android build shares
|
2026-08-07 22:08:35 -05:00
|
|
|
all app logic with the desktop; only data provisioning and file access differ.
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
|
2026-08-02 13:19:24 -05:00
|
|
|
Every Android command needs `ANDROID_HOME` / `NDK_HOME` / `JAVA_HOME`. Source
|
|
|
|
|
the helper once per shell — it auto-detects all three:
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-08-02 13:22:36 -05:00
|
|
|
source scripts/android-env.sh # bash / zsh
|
2026-08-02 13:19:24 -05:00
|
|
|
```
|
|
|
|
|
|
2026-08-02 13:22:36 -05:00
|
|
|
```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.
|
|
|
|
|
|
2026-08-02 13:19:24 -05:00
|
|
|
Then, from the project root:
|
|
|
|
|
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
```bash
|
2026-08-02 13:19:24 -05:00
|
|
|
cargo tauri android dev --open # dev server + open in Android Studio
|
|
|
|
|
cargo tauri android build --apk # standalone release APK
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`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.
|
|
|
|
|
|
2026-08-02 13:19:24 -05:00
|
|
|
### 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.
|
|
|
|
|
|
2026-08-02 13:27:24 -05:00
|
|
|
> **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.
|
|
|
|
|
|
2026-08-02 13:19:24 -05:00
|
|
|
To build without Studio and without a dev server, use `cargo tauri android
|
|
|
|
|
build` — that path needs no addr file.
|
|
|
|
|
|
2026-10-13 21:20:28 -05:00
|
|
|
### Distributing to phones (F-Droid)
|
|
|
|
|
|
|
|
|
|
Releases go out through a small F-Droid repo you host yourself: add it to the
|
|
|
|
|
F-Droid app once, and every new build then shows up there as an update.
|
|
|
|
|
`scripts/fdroid-publish.sh` builds a signed arm64 APK and adds it to the repo
|
|
|
|
|
in a directory you choose. It needs fdroidserver:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pipx install fdroidserver
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The first run also makes the app's signing key and the repo. Give it the
|
|
|
|
|
address the repo will be served at:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
FDROID_REPO_URL=https://fdroid.example.com/repo ./scripts/fdroid-publish.sh --init-keystore ~/fdroid-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After that, publishing is just:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./scripts/fdroid-publish.sh ~/fdroid-mtgc # or: npm run android:publish -- ~/fdroid-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It ends by printing the repo address with its fingerprint
|
|
|
|
|
(`https://…/repo?fingerprint=…`); add that in F-Droid under **Settings →
|
|
|
|
|
Repositories**.
|
|
|
|
|
|
|
|
|
|
- **Serve only `~/fdroid-mtgc/repo`** (and `archive/`, which holds older
|
|
|
|
|
versions, if you like) — any static file server will do. Never serve the
|
|
|
|
|
directory itself: it holds the repo signing key (`keystore.p12`) and its
|
|
|
|
|
password (`config.yml`).
|
|
|
|
|
- **Back up both keys**: `~/.local/share/mtgc/android-release.jks` with
|
|
|
|
|
`src-tauri/gen/android/keystore.properties` (the app key and its password),
|
|
|
|
|
and the repo directory's `keystore.p12` with `config.yml`. Android only
|
|
|
|
|
installs an update signed with the same key as the installed app, so
|
|
|
|
|
without the app key every phone would have to uninstall and reinstall.
|
|
|
|
|
- **Versions are automatic.** The Android versionCode is the number of commits
|
2026-10-13 22:05:17 -05:00
|
|
|
on `HEAD`, and the version shown is `0.1.<that count>`, e.g. `0.1.47`, so
|
|
|
|
|
any commit can ship without a bump. The major and minor come from
|
|
|
|
|
`tauri.conf.json`. That's also why pushed history shouldn't be
|
2026-10-13 21:20:28 -05:00
|
|
|
rewritten. The script won't publish uncommitted changes without
|
|
|
|
|
`--allow-dirty`, or a versionCode the repo already has.
|
|
|
|
|
- A phone with a debug build, or anything signed with another key, has to
|
|
|
|
|
uninstall it before F-Droid can install the release, and uninstalling
|
|
|
|
|
deletes the app's data. Sync or **Export library…** first.
|
|
|
|
|
|
|
|
|
|
Without `keystore.properties`, `cargo tauri android build` still produces an
|
|
|
|
|
unsigned APK as before.
|
|
|
|
|
|
Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.
Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
pinned rather than "latest" because sdkmanager's newest is often a beta
(30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.
Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.
Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
and the delete-first window could leave no reference.db at all if the process
was killed -- much likelier on Android.
UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.
Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00
|
|
|
### 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.
|
Rebrand to mtgc: pirate-ship logo, lowercase app name, mtgc binary
- New logomark: an original pirate-ship silhouette (hull, twin billowing
sails, swallow-tail pennant) in the terracotta brand gradient on a dark
square — a nod to the MTG card "Pirate Ship", not a reproduction of its
art. Source at src-tauri/icons/logo-source.svg; regenerate the full
platform icon set (desktop/Windows Store/iOS/Android) with
`cargo tauri icon src-tauri/icons/logo-source.svg`.
- Apply it in-app via a new inline ShipLogo component (App.tsx), replacing
the old "M" sidebar mark and "✦" empty-state mark with dark rounded
squares containing the ship glyph.
- Rename the app to lowercase "mtgc" everywhere: window title and
productName (tauri.conf.json), <title> (index.html), sidebar brand text,
empty-state heading.
- Rename the Tauri package/binary mtgc-app -> mtgc (and its lib crate
mtgc_app_lib -> mtgc_lib), so `cargo tauri build` now produces
target/release/mtgc; updated main.rs and every README reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:53:36 -05:00
|
|
|
|
2026-08-07 22:08:35 -05:00
|
|
|
### Sync on Android
|
|
|
|
|
|
Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
2026-10-08 22:32:08 -05:00
|
|
|
Signing in works as on desktop: the browser opens the server's sign-in page,
|
|
|
|
|
and returns to the app through the `mtgc://` URL scheme (registered by the
|
|
|
|
|
deep-link plugin from `plugins.deep-link` in `tauri.conf.json`). The app needs
|
|
|
|
|
no storage permission — the library lives in its private storage, and syncing
|
|
|
|
|
is network only.
|
2026-08-07 22:08:35 -05:00
|
|
|
|
Rebrand to mtgc: pirate-ship logo, lowercase app name, mtgc binary
- New logomark: an original pirate-ship silhouette (hull, twin billowing
sails, swallow-tail pennant) in the terracotta brand gradient on a dark
square — a nod to the MTG card "Pirate Ship", not a reproduction of its
art. Source at src-tauri/icons/logo-source.svg; regenerate the full
platform icon set (desktop/Windows Store/iOS/Android) with
`cargo tauri icon src-tauri/icons/logo-source.svg`.
- Apply it in-app via a new inline ShipLogo component (App.tsx), replacing
the old "M" sidebar mark and "✦" empty-state mark with dark rounded
squares containing the ship glyph.
- Rename the app to lowercase "mtgc" everywhere: window title and
productName (tauri.conf.json), <title> (index.html), sidebar brand text,
empty-state heading.
- Rename the Tauri package/binary mtgc-app -> mtgc (and its lib crate
mtgc_app_lib -> mtgc_lib), so `cargo tauri build` now produces
target/release/mtgc; updated main.rs and every README reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:53:36 -05:00
|
|
|
## 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
|
|
|
|
|
```
|