use serde::{Deserialize, Serialize}; use tauri::AppHandle; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectEntry { pub id: String, pub title: String, pub element_count: usize, pub saved_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProjectIndex { pub projects: Vec, pub active_id: Option, } const INDEX_FILE: &str = "projects_index.json"; fn project_file(id: &str) -> String { format!("project_{id}.json") } #[tauri::command] pub fn projects_list(app: AppHandle) -> Result, String> { let index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; Ok(index.projects) } #[tauri::command] pub fn projects_get_active_id(app: AppHandle) -> Result, String> { let index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; Ok(index.active_id) } /// Create a new project. Returns its id. Generates unique title if needed. #[tauri::command] pub fn project_create(app: AppHandle, title: Option) -> Result { let mut index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; // Generate unique title let base_title = title.unwrap_or_else(|| "Untitled Board".to_string()); let mut final_title = base_title.clone(); let mut counter = 2; while index.projects.iter().any(|p| p.title == final_title) { final_title = format!("{base_title} {counter}"); counter += 1; } let id = Uuid::new_v4().to_string(); let now = chrono::Utc::now().timestamp(); let entry = ProjectEntry { id: id.clone(), title: final_title, element_count: 0, saved_at: now }; // Save empty project file crate::persistence::save_json(&app, &project_file(&id), &serde_json::json!({ "title": &entry.title }))?; // Update index index.projects.insert(0, entry.clone()); index.active_id = Some(id); crate::persistence::save_json(&app, INDEX_FILE, &index)?; Ok(entry) } /// Save project state. Updates the project file and index metadata. #[tauri::command] pub fn project_save(app: AppHandle, id: String, state: String, title: Option) -> Result<(), String> { // Parse to count elements let parsed: serde_json::Value = serde_json::from_str(&state).unwrap_or(serde_json::json!({})); let images_count = parsed.get("images").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); let notes_count = parsed.get("textNotes").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); let annotations_count = parsed.get("annotations").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); let element_count = images_count + notes_count + annotations_count; // Save project file crate::persistence::save_json(&app, &project_file(&id), &parsed)?; // Update index let mut index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; let now = chrono::Utc::now().timestamp(); if let Some(entry) = index.projects.iter_mut().find(|p| p.id == id) { entry.element_count = element_count; entry.saved_at = now; if let Some(t) = title { entry.title = t; } } index.active_id = Some(id); crate::persistence::save_json(&app, INDEX_FILE, &index)?; Ok(()) } /// Load a project's full state JSON. #[tauri::command] pub fn project_load(app: AppHandle, id: String) -> Result { let path = crate::persistence::app_file_path(&app, &project_file(&id))?; if !path.exists() { return Ok("{}".to_string()); } let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; // Set as active let mut index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; index.active_id = Some(id); crate::persistence::save_json(&app, INDEX_FILE, &index)?; Ok(content) } /// Delete a project. #[tauri::command] pub fn project_delete(app: AppHandle, id: String) -> Result<(), String> { let mut index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; index.projects.retain(|p| p.id != id); if index.active_id.as_deref() == Some(&id) { index.active_id = index.projects.first().map(|p| p.id.clone()); } crate::persistence::save_json(&app, INDEX_FILE, &index)?; // Delete file (ignore if missing) let path = crate::persistence::app_file_path(&app, &project_file(&id))?; let _ = std::fs::remove_file(path); Ok(()) } /// Rename a project. #[tauri::command] pub fn project_rename(app: AppHandle, id: String, title: String) -> Result<(), String> { let mut index: ProjectIndex = crate::persistence::load_json(&app, INDEX_FILE)?; if let Some(entry) = index.projects.iter_mut().find(|p| p.id == id) { entry.title = title; } crate::persistence::save_json(&app, INDEX_FILE, &index) }