File size: 4,971 Bytes
3d7d9b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 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<ProjectEntry>,
pub active_id: Option<String>,
}
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<Vec<ProjectEntry>, 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<Option<String>, 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<String>) -> Result<ProjectEntry, String> {
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<String>) -> 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<String, String> {
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)
}
|