use std::path::{Path, PathBuf}; use serde::Serialize; use tauri::State; use crate::db::{self, ProjectRecord}; use crate::diagram::{self, DiagramInfo}; use crate::error::{AppError, Result}; use crate::git_ops::{self, BranchInfo, CommitInfo, FileStatus, RemoteStatus}; use crate::project::{self, ProjectConfig}; use crate::state::AppState; /// Everything the frontend needs after opening or creating a project. #[derive(Serialize)] pub struct OpenedProject { pub record: ProjectRecord, pub config: ProjectConfig, pub diagrams: Vec, pub branch: String, } fn now() -> String { chrono::Utc::now().to_rfc3339() } fn record_from_config(config: &ProjectConfig, path: &Path) -> ProjectRecord { ProjectRecord { id: config.project.id.clone(), name: config.project.name.clone(), path: path.to_string_lossy().to_string(), created_at: config.project.created_at.clone(), last_opened_at: Some(now()), } } // --------------------------------------------------------------------------- // Project registry & lifecycle // --------------------------------------------------------------------------- #[tauri::command] pub async fn list_projects(state: State<'_, AppState>) -> Result> { db::list_projects(&state.db).await } #[tauri::command] pub async fn create_project( state: State<'_, AppState>, parent_dir: String, name: String, description: String, ) -> Result { let dir = project::project_path_for(Path::new(&parent_dir), &name); let config = project::create_project(&dir, &name, &description)?; let record = record_from_config(&config, &dir); db::upsert_project(&state.db, &record).await?; let diagrams = diagram::list_diagrams(&dir)?; let branch = git_ops::current_branch(&dir).unwrap_or_else(|_| "main".into()); Ok(OpenedProject { record, config, diagrams, branch, }) } #[tauri::command] pub async fn open_project(state: State<'_, AppState>, path: String) -> Result { let dir = PathBuf::from(&path); if !project::is_project(&dir) { return Err(AppError::NotAProject(path)); } let config = project::read_config(&dir)?; let record = record_from_config(&config, &dir); db::upsert_project(&state.db, &record).await?; db::touch_project(&state.db, &record.path, &now()).await?; let diagrams = diagram::list_diagrams(&dir)?; let branch = git_ops::current_branch(&dir).unwrap_or_else(|_| "main".into()); Ok(OpenedProject { record, config, diagrams, branch, }) } /// Remove a project from the registry. Files on disk are left untouched. #[tauri::command] pub async fn forget_project(state: State<'_, AppState>, id: String) -> Result<()> { db::remove_project(&state.db, &id).await } // --------------------------------------------------------------------------- // Diagrams // --------------------------------------------------------------------------- #[tauri::command] pub fn list_diagrams(path: String) -> Result> { diagram::list_diagrams(Path::new(&path)) } #[tauri::command] pub fn read_diagram(path: String, rel: String) -> Result { diagram::read_diagram(Path::new(&path), &rel) } #[tauri::command] pub fn save_diagram(path: String, rel: String, content: String) -> Result<()> { diagram::save_diagram(Path::new(&path), &rel, &content) } #[tauri::command] pub fn create_diagram(path: String, name: String) -> Result { diagram::create_diagram(Path::new(&path), &name) } #[tauri::command] pub fn rename_diagram(path: String, rel: String, new_name: String) -> Result { diagram::rename_diagram(Path::new(&path), &rel, &new_name) } #[tauri::command] pub fn delete_diagram(path: String, rel: String) -> Result<()> { diagram::delete_diagram(Path::new(&path), &rel) } // --------------------------------------------------------------------------- // Git // --------------------------------------------------------------------------- #[tauri::command] pub fn git_status(path: String) -> Result> { git_ops::status(Path::new(&path)) } #[tauri::command] pub fn git_commit(path: String, message: String) -> Result { let msg = message.trim(); if msg.is_empty() { return Err(AppError::other("commit message cannot be empty")); } git_ops::commit_all(Path::new(&path), msg) } #[tauri::command] pub fn git_history(path: String, limit: Option) -> Result> { git_ops::history(Path::new(&path), limit.unwrap_or(100)) } #[tauri::command] pub fn git_branches(path: String) -> Result> { git_ops::branches(Path::new(&path)) } #[tauri::command] pub fn git_current_branch(path: String) -> Result { git_ops::current_branch(Path::new(&path)) } #[tauri::command] pub fn git_create_branch(path: String, name: String) -> Result<()> { let name = name.trim(); if name.is_empty() { return Err(AppError::other("branch name cannot be empty")); } git_ops::create_branch(Path::new(&path), name) } #[tauri::command] pub fn git_checkout_branch(path: String, name: String) -> Result<()> { git_ops::checkout_branch(Path::new(&path), &name) } // ---- Remote sync ---------------------------------------------------------- #[tauri::command] pub fn git_remote_status(path: String) -> Result { git_ops::remote_status(Path::new(&path)) } #[tauri::command] pub fn git_set_remote(path: String, url: String) -> Result<()> { let url = url.trim(); if url.is_empty() { return Err(AppError::other("remote URL cannot be empty")); } git_ops::set_remote(Path::new(&path), url) } #[tauri::command] pub fn git_fetch(path: String) -> Result { git_ops::fetch(Path::new(&path)) } #[tauri::command] pub fn git_pull(path: String) -> Result { git_ops::pull(Path::new(&path)) } #[tauri::command] pub fn git_push(path: String) -> Result { git_ops::push(Path::new(&path)) } // --------------------------------------------------------------------------- // Export helpers & settings // --------------------------------------------------------------------------- /// Write UTF-8 text to an arbitrary path chosen by the user (e.g. SVG export). #[tauri::command] pub fn write_text_file(path: String, contents: String) -> Result<()> { std::fs::write(path, contents)?; Ok(()) } /// Write raw bytes to a path chosen by the user (e.g. PNG export). #[tauri::command] pub fn write_binary_file(path: String, contents: Vec) -> Result<()> { std::fs::write(path, contents)?; Ok(()) } #[tauri::command] pub async fn get_setting(state: State<'_, AppState>, key: String) -> Result> { db::get_setting(&state.db, &key).await } #[tauri::command] pub async fn set_setting( state: State<'_, AppState>, key: String, value: String, ) -> Result<()> { db::set_setting(&state.db, &key, &value).await }