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
|
|
|
//! Tauri commands: the typed IPC surface bridging the web UI to `mtgc-core`.
|
|
|
|
|
//!
|
|
|
|
|
//! JS passes camelCase argument keys; Tauri maps them to these snake_case params.
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::Mutex;
|
|
|
|
|
|
2026-07-21 21:04:36 -05:00
|
|
|
use mtgc_core::{
|
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
|
|
|
CardRef, CardSuggestion, Deck, DeckEntry, LibraryCard, LibraryCopy, LibraryFacets,
|
|
|
|
|
LibraryFilter, LibraryStats, PrintingRef, SuggestOptions,
|
2026-07-21 21:04:36 -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
|
|
|
use serde::Serialize;
|
|
|
|
|
use tauri::{AppHandle, Emitter, State};
|
|
|
|
|
|
|
|
|
|
use crate::state::AppState;
|
|
|
|
|
|
|
|
|
|
type Db<'a> = State<'a, Mutex<AppState>>;
|
|
|
|
|
type Res<T> = Result<T, String>;
|
|
|
|
|
|
|
|
|
|
/// High-level status for the top bar / first-run gating.
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct Status {
|
2026-10-16 21:44:55 -05:00
|
|
|
/// The open library's name and where it syncs: the account and server, or
|
|
|
|
|
/// this device only. Changes whenever another library is opened.
|
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
|
|
|
pub library_label: String,
|
2026-10-16 21:44:55 -05:00
|
|
|
/// The open library.
|
|
|
|
|
pub library: OpenLibrary,
|
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
|
|
|
pub reference_path: String,
|
|
|
|
|
pub reference_ready: bool,
|
|
|
|
|
pub card_count: usize,
|
2026-07-07 21:35:09 -05:00
|
|
|
/// Unix timestamp (seconds) of reference.db's last modification — i.e.
|
|
|
|
|
/// when "Update card data" last completed. `None` if unavailable.
|
|
|
|
|
pub reference_updated_at: Option<i64>,
|
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
|
|
|
/// Columns this build's card-data schema has that the installed card data
|
|
|
|
|
/// lacks (`"table.column"`). Non-empty means it was built by an older
|
|
|
|
|
/// version and should be updated; empty when current or not loaded.
|
|
|
|
|
pub reference_missing_columns: Vec<String>,
|
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
|
|
|
/// Whether this is a mobile build. The UI uses it to hide affordances that
|
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
|
|
|
/// only make sense with an arbitrary-path filesystem. Reported from the build
|
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
|
|
|
/// target rather than sniffed from the user agent so it can't disagree with
|
|
|
|
|
/// what the backend will actually allow.
|
|
|
|
|
pub is_mobile: bool,
|
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
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct OpenLibrary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The open library and where it syncs, for the top bar — never the internal
|
|
|
|
|
/// working database, which is an implementation detail the user never sees.
|
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
|
|
|
fn library_label(s: &AppState) -> String {
|
2026-10-16 21:44:55 -05:00
|
|
|
let lib = s.active_library();
|
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
|
|
|
match s.sync_account() {
|
2026-10-16 21:44:55 -05:00
|
|
|
Some(a) if a.token.is_some() && lib.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
|
|
|
let host = url::Url::parse(&a.server_url)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|u| u.host_str().map(str::to_string))
|
|
|
|
|
.unwrap_or_else(|| a.server_url.clone());
|
2026-10-16 21:44:55 -05:00
|
|
|
format!("{} · {} @ {host}", lib.name, a.user.username)
|
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-16 21:44:55 -05:00
|
|
|
_ => format!("{} · this device only", lib.name),
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
fn status_of(s: &AppState) -> Status {
|
2026-07-07 21:35:09 -05:00
|
|
|
let reference_updated_at = std::fs::metadata(&s.reference_path)
|
|
|
|
|
.and_then(|m| m.modified())
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
|
|
|
.map(|d| d.as_secs() as i64);
|
2026-10-16 21:44:55 -05:00
|
|
|
let lib = s.active_library();
|
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
|
|
|
Status {
|
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
|
|
|
library_label: library_label(s),
|
2026-10-16 21:44:55 -05:00
|
|
|
library: OpenLibrary { id: lib.id.clone(), name: lib.name.clone() },
|
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
|
|
|
reference_path: s.reference_path.display().to_string(),
|
|
|
|
|
reference_ready: s.catalog.is_some(),
|
|
|
|
|
card_count: s.index.as_ref().map(|i| i.len()).unwrap_or(0),
|
2026-07-07 21:35:09 -05:00
|
|
|
reference_updated_at,
|
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
|
|
|
reference_missing_columns: s
|
|
|
|
|
.catalog
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|c| c.missing_reference_columns().to_vec())
|
|
|
|
|
.unwrap_or_default(),
|
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
|
|
|
is_mobile: cfg!(mobile),
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_status(db: Db) -> Res<Status> {
|
2026-08-02 16:10:25 -05:00
|
|
|
let status = status_of(&db.lock().unwrap());
|
|
|
|
|
// Only ever called once per launch (from the frontend's one status-fetch
|
|
|
|
|
// effect, retried on failure/timeout) — cheap to log unconditionally, and
|
|
|
|
|
// it's the one way to tell "this invoke never reached Rust" (nothing
|
|
|
|
|
// logged at all) apart from "it reached Rust and returned not-ready".
|
|
|
|
|
crate::mlog!(
|
|
|
|
|
"get_status: reference_ready={} card_count={}",
|
|
|
|
|
status.reference_ready,
|
|
|
|
|
status.card_count
|
|
|
|
|
);
|
|
|
|
|
Ok(status)
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Download Scryfall bulk data and (re)build reference.db, then reopen.
|
|
|
|
|
/// Runs on a blocking thread so the UI stays responsive and progress events flow.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn update_card_data(app: AppHandle, db: Db<'_>) -> Res<Status> {
|
|
|
|
|
let reference_path = db.lock().unwrap().reference_path.clone();
|
2026-07-07 20:22:31 -05:00
|
|
|
let cancel = mtgc_core::ingest::CancelToken::new();
|
|
|
|
|
db.lock().unwrap().ingest_cancel = Some(cancel.clone());
|
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
|
|
|
let app_for_progress = app.clone();
|
|
|
|
|
|
Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"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.
2026-08-02 14:00:04 -05:00
|
|
|
crate::mlog!("update_card_data: starting ingest into {}", reference_path.display());
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
let result = tauri::async_runtime::spawn_blocking({
|
|
|
|
|
let cancel = cancel.clone();
|
|
|
|
|
move || {
|
|
|
|
|
mtgc_core::ingest::build_reference_db(&reference_path, &cancel, |p| {
|
|
|
|
|
let _ = app_for_progress.emit("ingest-progress", p);
|
|
|
|
|
})
|
|
|
|
|
}
|
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
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
db.lock().unwrap().ingest_cancel = None;
|
Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"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.
2026-08-02 14:00:04 -05:00
|
|
|
let stats = crate::logged("update_card_data", result.map_err(|e| e.to_string()))?;
|
|
|
|
|
crate::mlog!(
|
|
|
|
|
"update_card_data: ok ({} oracle cards, {} printings)",
|
|
|
|
|
stats.oracle_cards,
|
|
|
|
|
stats.printings
|
|
|
|
|
);
|
2026-07-07 20:22:31 -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
|
|
|
let mut s = db.lock().unwrap();
|
Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"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.
2026-08-02 14:00:04 -05:00
|
|
|
crate::logged("update_card_data reopen", s.reopen())?;
|
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
|
|
|
Ok(status_of(&s))
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/// Adopt an already-built `reference.db` from elsewhere on the device instead of
|
|
|
|
|
/// downloading and rebuilding it.
|
|
|
|
|
///
|
|
|
|
|
/// This is the practical way to provision card data on mobile: the built db is
|
|
|
|
|
/// ~73MB, whereas `update_card_data` streams ~740MB of JSON and can take many
|
|
|
|
|
/// minutes — and Android may kill the app partway through. Copy the file from a
|
|
|
|
|
/// desktop install and point this at it.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn import_reference_db(db: Db, path: String) -> Res<Status> {
|
|
|
|
|
let src = PathBuf::from(&path);
|
|
|
|
|
let mut s = db.lock().unwrap();
|
|
|
|
|
let dest = s.reference_path.clone();
|
|
|
|
|
|
|
|
|
|
if src == dest {
|
|
|
|
|
return Err("That file is already the active card database.".into());
|
|
|
|
|
}
|
|
|
|
|
|
Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"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.
2026-08-02 14:00:04 -05:00
|
|
|
crate::mlog!("import_reference_db: from {}", src.display());
|
|
|
|
|
|
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
|
|
|
// Validate before disturbing the working db, so picking the wrong file is a
|
|
|
|
|
// harmless error rather than a broken install.
|
Fix card-data download for Scryfall's new gzipped-JSONL bulk API
"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.
2026-08-02 14:00:04 -05:00
|
|
|
let cards = crate::logged(
|
|
|
|
|
"import_reference_db validate",
|
|
|
|
|
mtgc_core::db::inspect_reference_db(&src).map_err(|e| e.to_string()),
|
|
|
|
|
)?;
|
|
|
|
|
crate::mlog!("import_reference_db: source holds {cards} oracle cards");
|
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
|
|
|
|
|
|
|
|
// Release the handles on the file we're about to replace.
|
|
|
|
|
s.close();
|
|
|
|
|
|
|
|
|
|
// Copy beside the destination, then rename: a same-directory rename is
|
|
|
|
|
// atomic, so an interrupted copy can't leave a half-written reference.db.
|
|
|
|
|
let tmp = dest.with_extension("db.import");
|
|
|
|
|
let install = || -> std::io::Result<()> {
|
|
|
|
|
if let Some(parent) = dest.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
std::fs::copy(&src, &tmp)?;
|
|
|
|
|
std::fs::rename(&tmp, &dest)
|
|
|
|
|
};
|
|
|
|
|
if let Err(e) = install() {
|
|
|
|
|
let _ = std::fs::remove_file(&tmp);
|
|
|
|
|
let _ = s.reopen(); // put the previous db back into service
|
|
|
|
|
return Err(format!("Couldn't install that card database: {e}"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.reopen()?;
|
|
|
|
|
Ok(status_of(&s))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 20:22:31 -05:00
|
|
|
/// Cancel an in-progress `update_card_data`, if one is running. No-op otherwise.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn cancel_update_card_data(db: Db) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
if let Some(c) = &s.ingest_cancel {
|
|
|
|
|
c.cancel();
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn search_cards(db: Db, query: String, limit: usize) -> Res<Vec<CardRef>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
Ok(s.index()?.search(&query, limit))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn add_owned(db: Db, oracle_id: String, delta: i64) -> Res<i64> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.add_owned(&oracle_id, delta).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_owned(db: Db, oracle_id: String, quantity: i64) -> Res<i64> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_owned(&oracle_id, quantity)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_location(db: Db, oracle_id: String, location: String) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_location(&oracle_id, &location)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn deck_contents(db: Db, deck_id: i64) -> Res<Vec<DeckEntry>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.deck_contents(deck_id).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
pub fn suggest_cards(
|
|
|
|
|
db: Db,
|
|
|
|
|
deck_id: i64,
|
|
|
|
|
offset: i64,
|
|
|
|
|
options: SuggestOptions,
|
|
|
|
|
) -> Res<Vec<CardSuggestion>> {
|
2026-07-21 21:04:36 -05:00
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
Spare/rarity filters, owned-only suggestions, and a Game night tab
Library filters:
- "Only spare copies" quick filter (Library): cards with more copies
owned than the decks' main boards use, the opposite of the ⚠
over-assignment warning. Rows show "N spare" while it's on.
- Rarity toggles (C/U/R/M) in the shared criteria panel, so Library,
Card Search and the set browser all get them. Any printing counts,
since the library tracks cards rather than printings; within a set
it's that set's printing. Arena-only printings don't count for a card
that also exists on paper, so a digital re-rarity can't make a paper
common read as rare.
Suggestions:
- "From my collection" toggle (remembered) narrows candidates to owned
cards before scoring and paging, so "Suggest more" keeps finding them.
- Each suggestion reports copies other decks already use, shown as
"N spare" / "all in other decks". suggest_cards now takes a
SuggestOptions struct instead of a bare `unusual` flag.
Game tab: a life counter for 2-6 players with tap/hold ±1/±10, a
running change bubble, poison and per-opponent commander damage (which
also comes off life), out-of-the-game markers, dice and a coin, and a
random first player. A seat's deck puts its commander's art behind the
life total. On phones the board fills the screen with the far row
turned to face across the table, and asks to keep the screen awake.
The game is per-device scratch state in localStorage, not synced.
Plain filter chips now show an accent background when active; before,
"Unusual picks" only changed its text colour.
2026-10-14 22:09:39 -05:00
|
|
|
.suggest_cards(deck_id, 50, offset.max(0) as usize, options)
|
2026-07-21 21:04:36 -05:00
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
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
|
|
|
pub fn search_library(db: Db, filter: LibraryFilter) -> Res<Vec<LibraryCard>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.search_library(&filter).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:55:17 -05:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn search_reference(db: Db, filter: LibraryFilter, limit: usize) -> Res<Vec<LibraryCard>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.search_reference(&filter, limit)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_library_stats(db: Db) -> Res<LibraryStats> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.library_stats().map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_library_facets(db: Db) -> Res<LibraryFacets> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.library_facets().map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:55:17 -05:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_reference_facets(db: Db) -> Res<LibraryFacets> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.reference_facets().map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn list_decks(db: Db) -> Res<Vec<Deck>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.list_decks().map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn create_deck(db: Db, name: String, commander_oracle_id: Option<String>) -> Res<Deck> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.create_deck(&name, commander_oracle_id.as_deref())
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn delete_deck(db: Db, deck_id: i64) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.delete_deck(deck_id).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn rename_deck(db: Db, deck_id: i64, name: String) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.rename_deck(deck_id, &name)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn duplicate_deck(db: Db, deck_id: i64, new_name: String) -> Res<Deck> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.duplicate_deck(deck_id, &new_name)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_deck_format(db: Db, deck_id: i64, format: String) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_deck_format(deck_id, &format)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_deck_commander(db: Db, deck_id: i64, oracle_id: Option<String>) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_deck_commander(deck_id, oracle_id.as_deref())
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_deck_card(
|
|
|
|
|
db: Db,
|
|
|
|
|
deck_id: i64,
|
|
|
|
|
oracle_id: String,
|
|
|
|
|
quantity: i64,
|
|
|
|
|
is_commander: bool,
|
2026-07-14 21:32:25 -05:00
|
|
|
is_maybeboard: bool,
|
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
|
|
|
) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
2026-07-14 21:32:25 -05:00
|
|
|
.set_deck_card(deck_id, &oracle_id, quantity, is_commander, is_maybeboard)
|
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
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn list_printings(db: Db, oracle_id: String) -> Res<Vec<PrintingRef>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.list_printings(&oracle_id).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn list_library_copies(db: Db, oracle_id: String) -> Res<Vec<LibraryCopy>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.list_library_copies(&oracle_id)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the card's owned quantity afterwards, which grows when the copy
|
|
|
|
|
/// groups would otherwise exceed it.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_library_copy(
|
|
|
|
|
db: Db,
|
|
|
|
|
oracle_id: String,
|
|
|
|
|
scryfall_id: String,
|
|
|
|
|
finish: String,
|
|
|
|
|
quantity: i64,
|
|
|
|
|
) -> Res<i64> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_library_copy(&oracle_id, &scryfall_id, &finish, quantity)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the slot afterwards, so the UI can show the resulting art without
|
|
|
|
|
/// knowing how the default is chosen.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_deck_card_printing(
|
|
|
|
|
db: Db,
|
|
|
|
|
deck_id: i64,
|
|
|
|
|
oracle_id: String,
|
|
|
|
|
scryfall_id: Option<String>,
|
|
|
|
|
) -> Res<Option<DeckEntry>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
let catalog = s.catalog()?;
|
|
|
|
|
catalog
|
|
|
|
|
.set_deck_card_printing(deck_id, &oracle_id, scryfall_id.as_deref())
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
let contents = catalog.deck_contents(deck_id).map_err(|e| e.to_string())?;
|
|
|
|
|
Ok(contents.into_iter().find(|e| e.card.oracle_id == oracle_id))
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn card_decks(db: Db, oracle_id: String) -> Res<Vec<Deck>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?.card_decks(&oracle_id).map_err(|e| e.to_string())
|
|
|
|
|
}
|
2026-07-20 00:21:22 -05:00
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct DeckImportResult {
|
|
|
|
|
pub deck: Deck,
|
|
|
|
|
pub unresolved: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct LibraryImportResult {
|
|
|
|
|
pub added: Vec<(String, i64)>,
|
|
|
|
|
pub unresolved: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn export_deck(db: Db, deck_id: i64, path: String) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
let text = s.catalog()?.export_deck_text(deck_id).map_err(|e| e.to_string())?;
|
|
|
|
|
std::fs::write(&path, text).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn import_deck(db: Db, path: String, name: String) -> Res<DeckImportResult> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
|
|
|
|
let (deck, unresolved) = s.catalog()?.import_deck(&name, &text).map_err(|e| e.to_string())?;
|
|
|
|
|
Ok(DeckImportResult { deck, unresolved })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn export_library_cards(db: Db, oracle_ids: Vec<String>, path: String) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
let text = s
|
|
|
|
|
.catalog()?
|
|
|
|
|
.export_library_text(&oracle_ids)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
std::fs::write(&path, text).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn import_library_cards(db: Db, path: String) -> Res<LibraryImportResult> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
|
|
|
|
let (added, unresolved) = s.catalog()?.import_library_list(&text).map_err(|e| e.to_string())?;
|
|
|
|
|
Ok(LibraryImportResult { added, unresolved })
|
|
|
|
|
}
|