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>
This commit is contained in:
2026-07-07 19:37:56 -05:00
commit 399c628461
94 changed files with 11626 additions and 0 deletions

58
src-tauri/src/state.rs Normal file
View File

@@ -0,0 +1,58 @@
//! App state: the open [`Catalog`] and fuzzy [`NameIndex`], plus the db paths.
use std::path::PathBuf;
use mtgc_core::{Catalog, NameIndex};
/// Managed application state, guarded by a `Mutex` in Tauri's state store.
pub struct AppState {
pub library_path: PathBuf,
pub reference_path: PathBuf,
pub catalog: Option<Catalog>,
pub index: Option<NameIndex>,
}
impl AppState {
/// Initialize with default paths under the app data dir and open the catalog
/// if a reference db already exists.
pub fn new(data_dir: PathBuf) -> Self {
let mut s = Self {
library_path: data_dir.join("library.db"),
reference_path: data_dir.join("reference.db"),
catalog: None,
index: None,
};
// Best-effort open; failures just leave the catalog unloaded.
let _ = s.reopen();
s
}
/// (Re)open the catalog + rebuild the name index. Only opens when a reference
/// db exists — attaching a non-existent file would create an empty (tableless)
/// db and make every query fail.
pub fn reopen(&mut self) -> Result<(), String> {
if self.reference_path.exists() {
let catalog = Catalog::open(&self.library_path, &self.reference_path)
.map_err(|e| e.to_string())?;
let index = catalog.build_name_index().map_err(|e| e.to_string())?;
self.index = Some(index);
self.catalog = Some(catalog);
} else {
self.catalog = None;
self.index = None;
}
Ok(())
}
pub fn catalog(&self) -> Result<&Catalog, String> {
self.catalog
.as_ref()
.ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string())
}
pub fn index(&self) -> Result<&NameIndex, String> {
self.index
.as_ref()
.ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string())
}
}