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;
|
|
|
|
|
|
|
|
|
|
use mtgc_core::{CardRef, Deck, DeckEntry, LibraryCard, LibraryFilter};
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn status_of(s: &AppState) -> Status {
|
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_status(db: Db) -> Res<Status> {
|
|
|
|
|
Ok(status_of(&db.lock().unwrap()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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();
|
|
|
|
|
|
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;
|
|
|
|
|
result.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
|
|
|
let mut s = db.lock().unwrap();
|
|
|
|
|
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]
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
) -> Res<()> {
|
|
|
|
|
let s = db.lock().unwrap();
|
|
|
|
|
s.catalog()?
|
|
|
|
|
.set_deck_card(deck_id, &oracle_id, quantity, is_commander)
|
|
|
|
|
.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())
|
|
|
|
|
}
|