2026-03-20 22:06:29 -07:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-03-20 22:17:35 -07:00
|
|
|
use std::fs;
|
2026-02-26 16:50:14 -08:00
|
|
|
use tauri::State;
|
|
|
|
|
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
use crate::db::models::Project;
|
2026-02-26 16:50:14 -08:00
|
|
|
use crate::db::queries;
|
|
|
|
|
use crate::state::AppState;
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
|
2026-03-20 22:17:35 -07:00
|
|
|
// ── File-based project types ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub struct ProjectFile {
|
|
|
|
|
pub version: u32,
|
|
|
|
|
pub name: String,
|
2026-03-23 09:48:27 -07:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub audio_file: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub source_file: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub audio_wav: Option<String>,
|
2026-03-20 22:17:35 -07:00
|
|
|
pub created_at: String,
|
|
|
|
|
pub segments: Vec<ProjectFileSegment>,
|
|
|
|
|
pub speakers: Vec<ProjectFileSpeaker>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub struct ProjectFileSegment {
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub speaker: Option<String>,
|
|
|
|
|
pub is_edited: bool,
|
|
|
|
|
pub words: Vec<ProjectFileWord>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub struct ProjectFileWord {
|
|
|
|
|
pub word: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub confidence: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub struct ProjectFileSpeaker {
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub display_name: Option<String>,
|
|
|
|
|
pub color: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:06:29 -07:00
|
|
|
// ── Input types for save_project_transcript ──────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct WordInput {
|
|
|
|
|
pub word: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub confidence: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct SegmentInput {
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub speaker: Option<String>, // speaker label, not id
|
|
|
|
|
pub words: Vec<WordInput>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct SpeakerInput {
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub color: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Output types for load_project_transcript ─────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct WordOutput {
|
|
|
|
|
pub word: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub confidence: Option<f64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct SegmentOutput {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub start_ms: i64,
|
|
|
|
|
pub end_ms: i64,
|
|
|
|
|
pub speaker: Option<String>, // speaker label
|
|
|
|
|
pub words: Vec<WordOutput>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct SpeakerOutput {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub display_name: Option<String>,
|
|
|
|
|
pub color: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct ProjectTranscript {
|
|
|
|
|
pub file_path: String,
|
|
|
|
|
pub segments: Vec<SegmentOutput>,
|
|
|
|
|
pub speakers: Vec<SpeakerOutput>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Commands ─────────────────────────────────────────────────────
|
|
|
|
|
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
#[tauri::command]
|
2026-02-26 16:50:14 -08:00
|
|
|
pub fn create_project(name: String, state: State<AppState>) -> Result<Project, String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
queries::create_project(&conn, &name).map_err(|e| e.to_string())
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2026-02-26 16:50:14 -08:00
|
|
|
pub fn get_project(id: String, state: State<AppState>) -> Result<Option<Project>, String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
queries::get_project(&conn, &id).map_err(|e| e.to_string())
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2026-02-26 16:50:14 -08:00
|
|
|
pub fn list_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
queries::list_projects(&conn).map_err(|e| e.to_string())
|
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
|
|
|
}
|
2026-03-20 22:06:29 -07:00
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn delete_project(id: String, state: State<AppState>) -> Result<(), String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
queries::delete_project(&conn, &id).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:08:52 -07:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn update_segment(
|
|
|
|
|
segment_id: String,
|
|
|
|
|
new_text: String,
|
|
|
|
|
state: State<AppState>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
queries::update_segment_text(&conn, &segment_id, &new_text).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:06:29 -07:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn save_project_transcript(
|
|
|
|
|
project_id: String,
|
|
|
|
|
file_path: String,
|
|
|
|
|
segments: Vec<SegmentInput>,
|
|
|
|
|
speakers: Vec<SpeakerInput>,
|
|
|
|
|
state: State<AppState>,
|
|
|
|
|
) -> Result<Project, String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// 1. Create media file entry
|
|
|
|
|
let media_file =
|
|
|
|
|
queries::create_media_file(&conn, &project_id, &file_path).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// 2. Create speaker entries and build label -> id map
|
|
|
|
|
let mut speaker_map = std::collections::HashMap::new();
|
|
|
|
|
for speaker_input in &speakers {
|
|
|
|
|
let speaker = queries::create_speaker(
|
|
|
|
|
&conn,
|
|
|
|
|
&project_id,
|
|
|
|
|
&speaker_input.label,
|
|
|
|
|
Some(&speaker_input.color),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
speaker_map.insert(speaker_input.label.clone(), speaker.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Create segments with words
|
|
|
|
|
for (index, seg_input) in segments.iter().enumerate() {
|
|
|
|
|
let speaker_id = seg_input
|
|
|
|
|
.speaker
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|label| speaker_map.get(label));
|
|
|
|
|
|
|
|
|
|
let segment = queries::create_segment(
|
|
|
|
|
&conn,
|
|
|
|
|
&project_id,
|
|
|
|
|
&media_file.id,
|
|
|
|
|
speaker_id.map(|s| s.as_str()),
|
|
|
|
|
seg_input.start_ms,
|
|
|
|
|
seg_input.end_ms,
|
|
|
|
|
&seg_input.text,
|
|
|
|
|
index as i32,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// Create words for this segment
|
|
|
|
|
for (word_index, word_input) in seg_input.words.iter().enumerate() {
|
|
|
|
|
queries::create_word(
|
|
|
|
|
&conn,
|
|
|
|
|
&segment.id,
|
|
|
|
|
&word_input.word,
|
|
|
|
|
word_input.start_ms,
|
|
|
|
|
word_input.end_ms,
|
|
|
|
|
Some(word_input.confidence),
|
|
|
|
|
word_index as i32,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Return updated project info
|
|
|
|
|
queries::get_project(&conn, &project_id)
|
|
|
|
|
.map_err(|e| e.to_string())?
|
|
|
|
|
.ok_or_else(|| "Project not found".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn load_project_transcript(
|
|
|
|
|
project_id: String,
|
|
|
|
|
state: State<AppState>,
|
|
|
|
|
) -> Result<Option<ProjectTranscript>, String> {
|
|
|
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// 1. Get media files for the project
|
|
|
|
|
let media_files =
|
|
|
|
|
queries::get_media_files_for_project(&conn, &project_id).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
let media_file = match media_files.first() {
|
|
|
|
|
Some(mf) => mf,
|
|
|
|
|
None => return Ok(None),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 2. Get speakers for the project and build id -> label map
|
|
|
|
|
let speakers =
|
|
|
|
|
queries::get_speakers_for_project(&conn, &project_id).map_err(|e| e.to_string())?;
|
|
|
|
|
let speaker_label_map: std::collections::HashMap<String, String> = speakers
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| (s.id.clone(), s.label.clone()))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// 3. Get segments for the media file
|
|
|
|
|
let db_segments =
|
|
|
|
|
queries::get_segments_for_media(&conn, &media_file.id).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// 4. Build output segments with nested words
|
|
|
|
|
let mut segment_outputs = Vec::with_capacity(db_segments.len());
|
|
|
|
|
for seg in &db_segments {
|
|
|
|
|
let words = queries::get_words_for_segment(&conn, &seg.id).map_err(|e| e.to_string())?;
|
|
|
|
|
let word_outputs: Vec<WordOutput> = words
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|w| WordOutput {
|
|
|
|
|
word: w.word,
|
|
|
|
|
start_ms: w.start_ms,
|
|
|
|
|
end_ms: w.end_ms,
|
|
|
|
|
confidence: w.confidence,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let speaker_label = seg
|
|
|
|
|
.speaker_id
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|sid| speaker_label_map.get(sid))
|
|
|
|
|
.cloned();
|
|
|
|
|
|
|
|
|
|
segment_outputs.push(SegmentOutput {
|
|
|
|
|
id: seg.id.clone(),
|
|
|
|
|
text: seg.text.clone(),
|
|
|
|
|
start_ms: seg.start_ms,
|
|
|
|
|
end_ms: seg.end_ms,
|
|
|
|
|
speaker: speaker_label,
|
|
|
|
|
words: word_outputs,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Build speaker outputs
|
|
|
|
|
let speaker_outputs: Vec<SpeakerOutput> = speakers
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|s| SpeakerOutput {
|
|
|
|
|
id: s.id,
|
|
|
|
|
label: s.label,
|
|
|
|
|
display_name: s.display_name,
|
|
|
|
|
color: s.color,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(Some(ProjectTranscript {
|
|
|
|
|
file_path: media_file.file_path.clone(),
|
|
|
|
|
segments: segment_outputs,
|
|
|
|
|
speakers: speaker_outputs,
|
|
|
|
|
}))
|
|
|
|
|
}
|
2026-03-20 22:17:35 -07:00
|
|
|
|
|
|
|
|
// ── File-based project commands ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn save_project_file(path: String, project: ProjectFile) -> Result<(), String> {
|
|
|
|
|
let json = serde_json::to_string_pretty(&project).map_err(|e| e.to_string())?;
|
|
|
|
|
fs::write(&path, json).map_err(|e| format!("Failed to save project: {e}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn load_project_file(path: String) -> Result<ProjectFile, String> {
|
|
|
|
|
let json = fs::read_to_string(&path).map_err(|e| format!("Failed to read project: {e}"))?;
|
|
|
|
|
serde_json::from_str(&json).map_err(|e| format!("Failed to parse project: {e}"))
|
|
|
|
|
}
|