On Android the open dialog returns a content:// URI, which std::fs can't
open, so ManaBox/library and deck imports failed with "no such file or
directory". Read them via tauri-plugin-fs, which resolves content URIs and
falls through to plain paths on desktop.
Closes#24
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.
The app knew nothing about the game outside your own collection — no way to
see that a set drops next week, or that a Secret Lair was announced.
Two sources with very different reliability, kept apart so one failing can't
take the page down.
Releases come from Scryfall's documented /sets endpoint. Clicking a set fetches
its cards live rather than reading reference.db, so a set released or spoiled
since the last card-data download still works; ownership is joined locally by
oracle_id, and the same add-to-library stepper works there as anywhere else.
Articles have no supported source at all: Wizards publishes no RSS or Atom feed
and no public API, and their page is a Nuxt app whose CSS classes are per-build
hashes. So the app links out rather than reproducing anyone's writing, which
keeps the scraped surface to titles and hrefs. parse_articles keys on nothing
cosmetic — anchors pointing at /en/news/<category>/<slug>, taking the anchor's
own text as the title. A restyle can't break that; only a change to their URL
scheme could, and the fixture tests fail loudly when it does rather than the
page quietly going empty. Verified against a live capture: 15 articles, correct
titles and categories.
Two things the fixtures caught. Where an article is linked several times per
card, the first link is a "Learn More" button, so the longest text wins rather
than the first. And a *successful* fetch yielding zero articles is treated as a
failure, since that is the signature of their markup changing.
get_set_cards returns a full CardRef rather than the handful of fields the grid
needs, so the tab renders the same CardTile as the rest of the app and gets
click-to-preview instead of a lookalike that does nothing when clicked.
Both feeds cache to news-cache.json for six hours and fall back to whatever was
last seen, so opening the tab offline shows stale content with its age rather
than an error.
Also promotes the HTTP agent out of ingest.rs into a shared http module, so
timeouts and User-Agent can't drift between the ingester and anything else that
reaches the network. Adds tauri-plugin-opener for opening articles externally.
"Download card data" had been failing on every platform with "missing field
`download_uri`". Scryfall changed the bulk-data API: entries now carry
`jsonl_download_uri`/`compressed_size` instead of `download_uri`/`size`, and
the payload is gzipped JSONL (one card object per line) rather than an
uncompressed JSON array. An existing reference.db keeps working, which is why
this went unnoticed.
- Rename the BulkEntry fields and decompress with flate2 (the response is
Content-Type: application/gzip with no Content-Encoding, so ureq won't
transparently inflate it). rust_backend keeps it pure Rust, so nothing new
has to cross-compile for Android.
- Replace the streaming JSON-array visitor with a line-based JSONL parser.
Closure errors now propagate directly instead of being laundered through
serde::de::Error::custom, so cancellation reports a clean Cancelled;
override_if_cancelled stays as a safety net and its doc comment is corrected.
- Progress wraps the compressed stream, since compressed_size is what the index
reports.
- Test fixtures stay readable pretty-printed JSON arrays via a jsonl() helper
that converts them, so tests still exercise the production parser.
Downloads are far smaller now: 101MB compressed (24 + 77) versus ~740MB, which
matters a lot on a phone.
Also log command failures to the platform log. Tauri command errors are
returned to the WebView as values, so they never reached logcat and the UI only
showed them truncated -- this bug was invisible until then. mlog! is a plain
eprintln!, which tao's Android glue forwards to logcat under RustStdoutStderr,
so no logging dependency is needed. View with: adb logcat -s RustStdoutStderr
Verified against the live API: 36,242 oracle cards, 116,409 printings, trigram
FTS working, and the aarch64 APK still builds.
- 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>