use std::{ collections::VecDeque, fs, path::{Path, PathBuf}, sync::Arc, }; use anyhow::{Context, Result}; use parking_lot::Mutex; use serde::Serialize; use serde_json::json; use tauri::{AppHandle, Emitter}; use crate::model::{AppConfig, AuthState, JobSummary, ScanEvent}; const MAX_JOBS: usize = 200; #[derive(Clone)] pub struct SharedState { pub config: Arc>, pub jobs: Arc>>, pub pending: Arc>>, pub auth: Arc>, pub data_dir: PathBuf, } impl SharedState { pub fn load(data_dir: PathBuf) -> Result { fs::create_dir_all(&data_dir).context("create FieldLens Print data directory")?; let mut config = read_json::(&data_dir.join("config.json")).unwrap_or_default(); if config.spool_dir.trim().is_empty() { config.spool_dir = data_dir.join("spool").to_string_lossy().into_owned(); } fs::create_dir_all(&config.spool_dir).ok(); let pending = read_json::>(&data_dir.join("pending.json")).unwrap_or_default(); let state = Self { config: Arc::new(Mutex::new(config)), jobs: Arc::new(Mutex::new(VecDeque::new())), pending: Arc::new(Mutex::new(pending)), auth: Arc::new(Mutex::new(AuthState::default())), data_dir, }; state.persist_config()?; Ok(state) } pub fn persist_config(&self) -> Result<()> { write_json_atomic(&self.data_dir.join("config.json"), &*self.config.lock()) } pub fn persist_pending(&self) -> Result<()> { write_json_atomic(&self.data_dir.join("pending.json"), &*self.pending.lock()) } pub fn replace_config(&self, next: AppConfig) -> Result { fs::create_dir_all(&next.spool_dir).ok(); *self.config.lock() = next.clone(); self.persist_config()?; Ok(next) } pub fn push_job(&self, app: &AppHandle, job: JobSummary) { { let mut jobs = self.jobs.lock(); jobs.push_front(job.clone()); while jobs.len() > MAX_JOBS { jobs.pop_back(); } } let _ = app.emit("capture-job", &job); } pub fn queue_scan( &self, raw_value: String, format: String, source: String, job_id: &str, label: &str, ) -> ScanEvent { let mut pending = self.pending.lock(); let duplicate = pending.iter().any(|event| event.raw_value == raw_value); let event = ScanEvent { id: uuid::Uuid::new_v4().to_string(), raw_value, format, duplicate, captured_at: chrono::Utc::now().to_rfc3339(), source, source_metadata: json!({ "jobId": job_id, "label": label, }), }; pending.push(event.clone()); drop(pending); let _ = self.persist_pending(); event } pub fn pending_count(&self) -> usize { self.pending.lock().len() } } fn read_json(path: &Path) -> Option { let bytes = fs::read(path).ok()?; serde_json::from_slice(&bytes).ok() } fn write_json_atomic(path: &Path, value: &T) -> Result<()> { let bytes = serde_json::to_vec_pretty(value)?; let temp = path.with_extension("tmp"); fs::write(&temp, bytes)?; if path.exists() { fs::remove_file(path)?; } fs::rename(&temp, path)?; Ok(()) }