- Exclude Scryfall Art Series cards (layout "art_series") from ingest — they aren't playable cards and were polluting search results. - Replace the coarse per-stage progress text with a byte-accurate, weighted multi-segment progress bar (Index / Card data / Printings / Indexing) that fills as one continuous bar instead of swapping per stage; segment weights are computed from the real download sizes reported by Scryfall's bulk-data index (the download responses themselves are chunked with no Content-Length to rely on). - Add a Cancel button, wired through a new CancelToken checked once per parsed record so cancellation lands almost instantly. - Fix a related correctness bug this surfaced: ingest used to delete the existing reference.db before writing its replacement, so a cancelled or crashed update left the app with no card database at all. It now builds into a temp file and only atomically replaces reference.db on full success. - Split the topbar's single "Choose library…" action into separate "Open Library…" (existing file) and "New Library…" (save-dialog for a new file) buttons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
181 lines
5.2 KiB
Rust
181 lines
5.2 KiB
Rust
//! 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();
|
|
s.library_path = PathBuf::from(path);
|
|
s.reopen()?;
|
|
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();
|
|
let cancel = mtgc_core::ingest::CancelToken::new();
|
|
db.lock().unwrap().ingest_cancel = Some(cancel.clone());
|
|
let app_for_progress = app.clone();
|
|
|
|
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);
|
|
})
|
|
}
|
|
})
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
db.lock().unwrap().ingest_cancel = None;
|
|
result.map_err(|e| e.to_string())?;
|
|
|
|
let mut s = db.lock().unwrap();
|
|
s.reopen()?;
|
|
Ok(status_of(&s))
|
|
}
|
|
|
|
/// 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(())
|
|
}
|
|
|
|
#[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())
|
|
}
|