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::{
|
|
|
|
|
CardRef, CardSuggestion, Deck, DeckEntry, LibraryCard, LibraryFacets, LibraryFilter,
|
|
|
|
|
LibraryStats,
|
|
|
|
|
};
|
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 {
|
|
|
|
|
pub library_path: String,
|
|
|
|
|
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>,
|
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
|
|
|
|
|
/// only make sense with an arbitrary-path filesystem (choosing where
|
|
|
|
|
/// library.db lives) — see `set_library_path`. Reported from the build
|
|
|
|
|
/// 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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
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 {
|
|
|
|
|
library_path: s.library_path.display().to_string(),
|
|
|
|
|
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,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn set_library_path(db: Db, path: String) -> Res<Status> {
|
|
|
|
|
let mut s = db.lock().unwrap();
|
2026-07-07 20:55:05 -05:00
|
|
|
s.set_library_path(PathBuf::from(path));
|
|
|
|
|
// Best-effort reopen: the chosen path must stick (and be reported back)
|
|
|
|
|
// even if the reference db can't currently be attached (e.g. stale
|
|
|
|
|
// schema pending an "Update card data" run) — that's a separate concern
|
|
|
|
|
// from which library file is open.
|
|
|
|
|
let _ = 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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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]
|
2026-07-21 21:04:36 -05:00
|
|
|
pub fn suggest_cards(db: Db, deck_id: i64, offset: i64, unusual: bool) -> Res<Vec<CardSuggestion>> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.suggest_cards(deck_id, 50, offset.max(0) as usize, unusual)
|
|
|
|
|
.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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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 })
|
|
|
|
|
}
|