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)
}