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
|
2026-10-16 21:44:55 -05:00
|
|
|
crates/mtgc-server/ Optional sync server: each user's libraries, 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
|
|
|
|
2026-10-16 21:44:55 -05:00
|
|
|
- **your libraries** — owned cards, decks, deck contents. Each lives on the
|
|
|
|
|
device and works fully offline; sign in to a sync server to share them
|
|
|
|
|
between devices (see [Sync](#sync)). You can keep several and switch between
|
|
|
|
|
them (see [Libraries](#libraries)). **Export library…** in the Info tab saves
|
|
|
|
|
the open one 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-22 21:19:23 -05:00
|
|
|
### Distributing to Debian/Ubuntu (apt repo)
|
|
|
|
|
|
|
|
|
|
Debian-based machines get the app the same way, from an apt repo you host.
|
|
|
|
|
`scripts/deb-publish.sh` builds the `.deb` here on Fedora and adds it to the
|
|
|
|
|
repo in a directory you choose. It needs:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo dnf install reprepro dpkg
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It signs with the same key as the dnf repo. If you haven't made that key yet,
|
|
|
|
|
pass `--init-key` the first time. Either way, give the first run the address
|
|
|
|
|
the directory will be served at:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
DEB_REPO_URL=https://deb.example.com/mtgc ./scripts/deb-publish.sh ~/deb-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After that, publishing is just:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./scripts/deb-publish.sh ~/deb-mtgc # or: npm run deb:publish -- ~/deb-mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then, on each machine, add the repo once and install:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo curl -fsSLo /etc/apt/sources.list.d/mtgc.sources https://deb.example.com/mtgc/mtgc.sources
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo apt update && sudo apt install mtgc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
From then on, `sudo apt upgrade` picks up new releases.
|
|
|
|
|
|
|
|
|
|
- **Serve the whole directory.** It holds `pool/`, the signed `dists/`, the
|
|
|
|
|
public key (`mtgc.asc`) and `mtgc.sources`, which carries the key inline so
|
|
|
|
|
machines don't need a separate keyring step. `conf/` and `db/` are
|
|
|
|
|
reprepro's own state; they're harmless to serve but aren't needed. The
|
|
|
|
|
newest three versions are kept, so a bad release can be undone with
|
|
|
|
|
`sudo apt install mtgc=<older version>`.
|
|
|
|
|
- Versions are the same as the RPM's. A build published with `--allow-dirty`
|
|
|
|
|
gets a `~dirty` suffix, which apt sorts below the clean build of the same
|
|
|
|
|
version.
|
|
|
|
|
- The `.deb` is built against this machine's libraries, so it needs a distro
|
|
|
|
|
with at least this Fedora's glibc (2.43 on Fedora 44). Older Debian and
|
|
|
|
|
Ubuntu releases can't install it.
|
|
|
|
|
|
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.
|
|
|
|
|
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
**Library → Import cards…** also takes a **ManaBox** export of a collection
|
|
|
|
|
or binder, either the `.txt` or the `.csv`. Both add to what you already own.
|
|
|
|
|
The CSV names each copy's exact printing and finish, so it's the better of
|
|
|
|
|
the two. Condition, language, prices and the other ManaBox fields aren't
|
2026-10-17 21:58:44 -05:00
|
|
|
kept. Proxies count as owned copies. After you pick the file, the import asks
|
|
|
|
|
for a location to file the cards under, pre-filled with the file's name (a
|
|
|
|
|
ManaBox export is named after its binder). Leave it blank to keep cards'
|
|
|
|
|
current locations.
|
ManaBox import, with progress and a format explainer
Library → Import cards… now reads a ManaBox collection export, either the
.csv or the .txt. The CSV is matched by Scryfall ID, falling back to name,
set and collector number, and its Foil column picks the copy group's finish.
Condition, language and prices are ignored; proxies count as owned.
A decklist line that names a printing now resolves to that printing's card
when several cards share the name (a card and its token, or an art-series
entry), and a bare name prefers the real card.
Imports run in one transaction (nestable via SAVEPOINT), which took the
942-row sample from 22s to 0.2s, and off the main thread, with a progress
dialog for slow ones. An explainer of the supported formats now comes
before the file picker.
2026-10-16 21:57:52 -05:00
|
|
|
|
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
|
|
|
> 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-10-16 21:58:28 -05:00
|
|
|
## Libraries
|
|
|
|
|
|
|
|
|
|
Every device starts with one library, **Main**. To keep others alongside it —
|
|
|
|
|
a scratch library to try things in, a cube, a trade binder — click the
|
|
|
|
|
library name at the left of the top bar and choose **New library…**. The same
|
|
|
|
|
menu switches between them; **Info → Libraries** lists them all, and is where
|
|
|
|
|
you rename or delete them (on a phone, it's the only place).
|
|
|
|
|
|
|
|
|
|
Each library has its own cards and decks; the selected deck and the
|
|
|
|
|
"location" for new cards are remembered per library too. Card data is shared.
|
|
|
|
|
|
|
|
|
|
A new library can **sync** to your other devices or stay on this one. Synced
|
|
|
|
|
libraries appear on every signed-in device by themselves; one kept on this
|
|
|
|
|
device only never leaves it, which makes it the safe place to experiment.
|
|
|
|
|
The **Sync** checkbox in **Info → Libraries** changes your mind either way.
|
|
|
|
|
|
|
|
|
|
Deleting a synced library asks whether to **remove it from this device** (it
|
|
|
|
|
stays on the server and on your other devices, and this one stops downloading
|
|
|
|
|
it) or **delete it everywhere**. Other devices keep their copy of a library
|
|
|
|
|
deleted everywhere, marked as deleted from the server, until you remove it or
|
|
|
|
|
put it back. **Main** can be renamed but not deleted.
|
|
|
|
|
|
|
|
|
|
Libraries' files live in the app-data dir: **Main** where the one library
|
|
|
|
|
always lived, the others under `libraries/`.
|
|
|
|
|
|
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.
|
|
|
|
|
|
2026-10-16 21:44:55 -05:00
|
|
|
Each library syncs on its own: a library's edits never reach another one. An
|
|
|
|
|
older sync server keeps just one library per account, which **Main** syncs
|
|
|
|
|
with; update the server to sync the others.
|
|
|
|
|
|
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
|
|
|
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.
|
|
|
|
|
|
2026-10-16 22:39:36 -05:00
|
|
|
> **Why `JAVA_HOME`?** The Android build (Gradle 9.6, AGP 9) runs on JDK 17
|
|
|
|
|
> through 25. The script prefers Android Studio's bundled JetBrains Runtime,
|
|
|
|
|
> then the newest system JDK in that range, and skips anything newer than 25
|
|
|
|
|
> until it's been tested.
|
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
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
```
|