//! 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, LibraryFacets, LibraryFilter, LibraryStats}; use serde::Serialize; use tauri::{AppHandle, Emitter, State}; use crate::state::AppState; type Db<'a> = State<'a, Mutex>; type Res = Result; /// 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, /// 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, } fn status_of(s: &AppState) -> Status { 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); 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), reference_updated_at, } } #[tauri::command] pub fn get_status(db: Db) -> Res { Ok(status_of(&db.lock().unwrap())) } #[tauri::command] pub fn set_library_path(db: Db, path: String) -> Res { let mut s = db.lock().unwrap(); 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(); 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 { 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> { 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 { 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 { 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> { 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> { let s = db.lock().unwrap(); s.catalog()?.search_library(&filter).map_err(|e| e.to_string()) } #[tauri::command] pub fn get_library_stats(db: Db) -> Res { let s = db.lock().unwrap(); s.catalog()?.library_stats().map_err(|e| e.to_string()) } #[tauri::command] pub fn get_library_facets(db: Db) -> Res { let s = db.lock().unwrap(); s.catalog()?.library_facets().map_err(|e| e.to_string()) } #[tauri::command] pub fn list_decks(db: Db) -> Res> { 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) -> Res { 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 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 { let s = db.lock().unwrap(); s.catalog()? .duplicate_deck(deck_id, &new_name) .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) -> 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, is_maybeboard: bool, ) -> Res<()> { let s = db.lock().unwrap(); s.catalog()? .set_deck_card(deck_id, &oracle_id, quantity, is_commander, is_maybeboard) .map_err(|e| e.to_string()) } #[tauri::command] pub fn card_decks(db: Db, oracle_id: String) -> Res> { let s = db.lock().unwrap(); s.catalog()?.card_decks(&oracle_id).map_err(|e| e.to_string()) } #[derive(Serialize)] pub struct DeckImportResult { pub deck: Deck, pub unresolved: Vec, } #[derive(Serialize)] pub struct LibraryImportResult { pub added: Vec<(String, i64)>, pub unresolved: Vec, } #[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 { 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, 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 { 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 }) }