Initialize repo

This commit is contained in:
Kazimierz Ciołek
2026-02-02 13:42:04 +01:00
commit 61e3482715
97 changed files with 156542 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
use crate::db::DbPool;
use crate::engine::analysis; // Import engine
use crate::error::CommandError;
use crate::models::{AnalysisSession, Annotation};
use tauri::State;
#[tauri::command]
pub fn get_analysis_sessions(
state: State<'_, DbPool>,
) -> Result<Vec<AnalysisSession>, CommandError> {
let conn = state.get()?;
analysis::get_sessions(&conn)
}
#[tauri::command]
pub fn create_analysis_session(
state: State<'_, DbPool>,
name: String,
video_path: Option<String>,
) -> Result<AnalysisSession, CommandError> {
let conn = state.get()?;
analysis::create_session(&conn, &name, video_path)
}
#[tauri::command]
pub fn delete_analysis_session(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
analysis::delete_session(&conn, &id)
}
// --- Annotations ---
#[tauri::command]
pub fn get_annotations(
state: State<'_, DbPool>,
session_id: String,
) -> Result<Vec<Annotation>, CommandError> {
let conn = state.get()?;
analysis::get_annotations(&conn, &session_id)
}
#[tauri::command]
pub fn add_annotation(
state: State<'_, DbPool>,
session_id: String,
start_time: f64,
end_time: f64,
name: String,
description: String,
color: String,
) -> Result<Annotation, CommandError> {
let conn = state.get()?;
analysis::add_annotation(
&conn,
&session_id,
start_time,
end_time,
&name,
&description,
&color,
)
}
#[tauri::command]
pub fn update_annotation(
state: State<'_, DbPool>,
annotation: Annotation,
) -> Result<(), CommandError> {
let conn = state.get()?;
analysis::update_annotation(&conn, &annotation)
}
#[tauri::command]
pub fn delete_annotation(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
analysis::delete_annotation(&conn, &id)
}
// --- Utils (Video Picker) ---
#[tauri::command]
pub async fn pick_video_file() -> Result<Option<String>, CommandError> {
let file = rfd::AsyncFileDialog::new()
.add_filter("Video", &["mp4", "mov", "avi", "mkv", "webm"])
.pick_file()
.await;
match file {
Some(f) => Ok(Some(f.path().to_string_lossy().to_string())),
None => Ok(None),
}
}

View File

@@ -0,0 +1,56 @@
use crate::db::DbPool;
use crate::engine::chat;
use crate::error::CommandError;
use crate::models::{ChatMessage, ChatSession};
use tauri::State;
#[tauri::command]
pub fn create_chat_session(
state: State<'_, DbPool>,
title: Option<String>,
) -> Result<ChatSession, CommandError> {
let conn = state.get()?;
chat::create_session(&conn, title)
}
#[tauri::command]
pub fn get_chat_sessions(state: State<'_, DbPool>) -> Result<Vec<ChatSession>, CommandError> {
let conn = state.get()?;
chat::get_sessions(&conn)
}
#[tauri::command]
pub fn update_chat_title(
state: State<'_, DbPool>,
id: String,
title: String,
) -> Result<(), CommandError> {
let conn = state.get()?;
chat::update_title(&conn, &id, &title)
}
#[tauri::command]
pub fn delete_chat_session(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
chat::delete_session(&conn, &id)
}
#[tauri::command]
pub fn save_chat_message(
state: State<'_, DbPool>,
session_id: String,
role: String,
content: String,
) -> Result<ChatMessage, CommandError> {
let conn = state.get()?;
chat::save_message(&conn, &session_id, &role, &content)
}
#[tauri::command]
pub fn get_chat_history(
state: State<'_, DbPool>,
session_id: String,
) -> Result<Vec<ChatMessage>, CommandError> {
let conn = state.get()?;
chat::get_history(&conn, &session_id)
}

View File

@@ -0,0 +1,72 @@
use crate::db::DbPool;
use crate::error::CommandError;
use crate::models::Exercise;
use rusqlite::params;
use tauri::State;
use uuid::Uuid;
#[tauri::command]
pub fn get_exercises(state: State<'_, DbPool>) -> Result<Vec<Exercise>, CommandError> {
let conn = state.get()?;
let mut stmt =
conn.prepare("SELECT id, name, instructions, enrichment, tags, video_url FROM exercises")?;
let exercise_iter = stmt.query_map([], |row| {
Ok(Exercise {
id: row.get(0)?,
name: row.get(1)?,
instructions: row.get(2)?,
enrichment: row.get(3)?,
tags: row.get(4)?,
video_url: row.get(5)?,
})
})?;
let mut exercises = Vec::new();
for exercise in exercise_iter {
exercises.push(exercise?);
}
Ok(exercises)
}
#[tauri::command]
pub fn add_exercise(
state: State<'_, DbPool>,
name: String,
instructions: String,
enrichment: String,
tags: String,
video_url: String,
) -> Result<Exercise, CommandError> {
let conn = state.get()?;
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO exercises (id, name, instructions, enrichment, tags, video_url) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![id, name, instructions, enrichment, tags, video_url],
)?;
Ok(Exercise {
id,
name,
instructions,
enrichment,
tags,
video_url,
})
}
#[tauri::command]
pub fn update_exercise(state: State<'_, DbPool>, exercise: Exercise) -> Result<(), CommandError> {
let conn = state.get()?;
conn.execute(
"UPDATE exercises SET name = ?1, instructions = ?2, enrichment = ?3, tags = ?4, video_url = ?5 WHERE id = ?6",
params![exercise.name, exercise.instructions, exercise.enrichment, exercise.tags, exercise.video_url, exercise.id],
)?;
Ok(())
}
#[tauri::command]
pub fn delete_exercise(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
conn.execute("DELETE FROM exercises WHERE id = ?1", params![id])?;
Ok(())
}

View File

@@ -0,0 +1,5 @@
pub mod analysis;
pub mod chat;
pub mod exercises;
pub mod plans;
pub mod programs;

View File

@@ -0,0 +1,61 @@
use crate::db::DbPool;
use crate::error::CommandError;
use crate::models::TrainingPlan;
use rusqlite::params;
use tauri::State;
use uuid::Uuid;
#[tauri::command]
pub fn get_plans(state: State<'_, DbPool>) -> Result<Vec<TrainingPlan>, CommandError> {
println!("get_plans called");
let conn = state.get()?;
let mut stmt = conn.prepare("SELECT id, name, sections FROM training_plans")?;
let plan_iter = stmt.query_map([], |row| {
Ok(TrainingPlan {
id: row.get(0)?,
name: row.get(1)?,
sections: row.get(2)?,
})
})?;
let mut plans = Vec::new();
for plan in plan_iter {
plans.push(plan?);
}
println!("get_plans returning {} plans", plans.len());
Ok(plans)
}
#[tauri::command]
pub fn create_plan(
state: State<'_, DbPool>,
name: String,
sections: String,
) -> Result<TrainingPlan, CommandError> {
println!("create_plan called: {}", name);
let conn = state.get()?;
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO training_plans (id, name, sections) VALUES (?1, ?2, ?3)",
params![id, name, sections],
)?;
println!("create_plan inserted id: {}", id);
Ok(TrainingPlan { id, name, sections })
}
#[tauri::command]
pub fn update_plan(state: State<'_, DbPool>, plan: TrainingPlan) -> Result<(), CommandError> {
let conn = state.get()?;
conn.execute(
"UPDATE training_plans SET name = ?1, sections = ?2 WHERE id = ?3",
params![plan.name, plan.sections, plan.id],
)?;
Ok(())
}
#[tauri::command]
pub fn delete_plan(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
conn.execute("DELETE FROM training_plans WHERE id = ?1", params![id])?;
Ok(())
}

View File

@@ -0,0 +1,112 @@
use crate::db::DbPool;
use crate::engine::programs; // Import engine
use crate::error::CommandError;
use crate::models::{Program, Week, Workout};
use tauri::State;
// --- Programs ---
#[tauri::command]
pub fn get_programs(state: State<'_, DbPool>) -> Result<Vec<Program>, CommandError> {
let conn = state.get()?;
programs::get_programs(&conn)
}
#[tauri::command]
pub fn create_program(state: State<'_, DbPool>, name: String) -> Result<Program, CommandError> {
let conn = state.get()?;
programs::create_program(&conn, &name)
}
#[tauri::command]
pub fn delete_program(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
programs::delete_program(&conn, &id)
}
#[tauri::command]
pub fn duplicate_program(state: State<'_, DbPool>, id: String) -> Result<Program, CommandError> {
let mut conn = state.get()?;
programs::duplicate_program(&mut conn, &id)
}
// --- Weeks ---
#[tauri::command]
pub fn get_weeks(state: State<'_, DbPool>, program_id: String) -> Result<Vec<Week>, CommandError> {
let conn = state.get()?;
programs::get_weeks(&conn, &program_id)
}
#[tauri::command]
pub fn add_week(
state: State<'_, DbPool>,
program_id: String,
position: i32,
) -> Result<Week, CommandError> {
let conn = state.get()?;
programs::add_week(&conn, &program_id, position)
}
#[tauri::command]
pub fn update_week_note(
state: State<'_, DbPool>,
id: String,
notes: String,
) -> Result<(), CommandError> {
let conn = state.get()?;
programs::update_week_note(&conn, &id, &notes)
}
#[tauri::command]
pub fn delete_week(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
programs::delete_week(&conn, &id)
}
// --- Workouts ---
#[tauri::command]
pub fn get_workouts(
state: State<'_, DbPool>,
program_id: String,
) -> Result<Vec<Workout>, CommandError> {
let conn = state.get()?;
programs::get_workouts(&conn, &program_id)
}
#[tauri::command]
pub fn add_workout(
state: State<'_, DbPool>,
week_id: String,
program_id: String,
day: String,
type_: String,
ref_id: Option<String>,
name: String,
description: String,
) -> Result<Workout, CommandError> {
let conn = state.get()?;
programs::add_workout(
&conn,
&week_id,
&program_id,
&day,
&type_,
ref_id,
&name,
&description,
)
}
#[tauri::command]
pub fn update_workout(state: State<'_, DbPool>, workout: Workout) -> Result<(), CommandError> {
let conn = state.get()?;
programs::update_workout(&conn, &workout)
}
#[tauri::command]
pub fn delete_workout(state: State<'_, DbPool>, id: String) -> Result<(), CommandError> {
let conn = state.get()?;
programs::delete_workout(&conn, &id)
}

166
src-tauri/src/db.rs Normal file
View File

@@ -0,0 +1,166 @@
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use std::fs;
use tauri::{AppHandle, Manager};
use crate::error::CommandError;
pub type DbPool = Pool<SqliteConnectionManager>;
pub fn init_db(app_handle: &AppHandle) -> Result<DbPool, CommandError> {
let app_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| CommandError::Internal(e.to_string()))?;
if !app_dir.exists() {
fs::create_dir_all(&app_dir)?;
}
let db_path = app_dir.join("trainhub.db");
println!("Database path: {:?}", db_path);
let manager = SqliteConnectionManager::file(&db_path);
let pool = Pool::new(manager)?;
let conn = pool.get()?;
// PRAGMA settings for performance/integrity
conn.execute("PRAGMA foreign_keys = ON;", [])?;
// WAL mode removed to ensure data persistence stability in dev environment
// --- Migrations ---
// In a real app, use a migration tool like `refinery` or `sqlx`.
// For now, we keep the idempotent "CREATE TABLE IF NOT EXISTS" approach.
conn.execute(
"CREATE TABLE IF NOT EXISTS exercises (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
instructions TEXT,
enrichment TEXT,
tags TEXT,
video_url TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS training_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
sections TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS programs (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS program_weeks (
id TEXT PRIMARY KEY,
program_id TEXT NOT NULL,
position INTEGER NOT NULL,
notes TEXT,
FOREIGN KEY(program_id) REFERENCES programs(id) ON DELETE CASCADE
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS program_workouts (
id TEXT PRIMARY KEY,
week_id TEXT NOT NULL,
program_id TEXT NOT NULL,
day TEXT NOT NULL,
type TEXT NOT NULL,
ref_id TEXT,
name TEXT,
description TEXT,
completed BOOLEAN DEFAULT 0,
FOREIGN KEY(week_id) REFERENCES program_weeks(id) ON DELETE CASCADE,
FOREIGN KEY(program_id) REFERENCES programs(id) ON DELETE CASCADE
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS analysis_sessions (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
date TEXT NOT NULL,
video_path TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS annotations (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
start_time REAL NOT NULL,
end_time REAL NOT NULL,
name TEXT,
description TEXT,
color TEXT,
FOREIGN KEY(session_id) REFERENCES analysis_sessions(id) ON DELETE CASCADE
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS flashcard_sets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS flashcards (
id TEXT PRIMARY KEY,
set_id TEXT,
front TEXT NOT NULL,
back TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY(set_id) REFERENCES flashcard_sets(id) ON DELETE CASCADE
)",
[],
)?;
// Primitive Migration: Check if column exists, if not ignore error or handle gracefully
// Since sqlite doesn't support IF NOT EXISTS for columns easily without logic:
// We try to add it, if it fails (duplicate column), we ignore.
let _ = conn.execute("ALTER TABLE flashcards ADD COLUMN set_id TEXT", []);
conn.execute(
"CREATE TABLE IF NOT EXISTS chat_sessions (
id TEXT PRIMARY KEY,
title TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS chat_messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY(session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
)",
[],
)?;
Ok(pool)
}

View File

@@ -0,0 +1,112 @@
use crate::error::CommandError;
use crate::models::{AnalysisSession, Annotation};
use rusqlite::{params, Connection};
use uuid::Uuid;
pub fn get_sessions(conn: &Connection) -> Result<Vec<AnalysisSession>, CommandError> {
let mut stmt = conn
.prepare("SELECT id, name, date, video_path FROM analysis_sessions ORDER BY date DESC")?;
let rows = stmt.query_map([], |row| {
Ok(AnalysisSession {
id: row.get(0)?,
name: row.get(1)?,
date: row.get(2)?,
video_path: row.get(3)?,
})
})?;
let mut sessions = Vec::new();
for r in rows {
sessions.push(r?);
}
Ok(sessions)
}
pub fn create_session(
conn: &Connection,
name: &str,
video_path: Option<String>,
) -> Result<AnalysisSession, CommandError> {
let id = Uuid::new_v4().to_string();
let date = chrono::Local::now().to_rfc3339();
conn.execute(
"INSERT INTO analysis_sessions (id, name, date, video_path) VALUES (?1, ?2, ?3, ?4)",
params![id, name, date, video_path],
)?;
Ok(AnalysisSession {
id,
name: name.to_string(),
date,
video_path,
})
}
pub fn delete_session(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM analysis_sessions WHERE id = ?1", params![id])?;
Ok(())
}
pub fn get_annotations(
conn: &Connection,
session_id: &str,
) -> Result<Vec<Annotation>, CommandError> {
let mut stmt = conn.prepare("SELECT id, session_id, start_time, end_time, name, description, color FROM annotations WHERE session_id = ?1")?;
let rows = stmt.query_map(params![session_id], |row| {
Ok(Annotation {
id: row.get(0)?,
session_id: row.get(1)?,
start_time: row.get(2)?,
end_time: row.get(3)?,
name: row.get(4)?,
description: row.get(5)?,
color: row.get(6)?,
})
})?;
let mut list = Vec::new();
for r in rows {
list.push(r?);
}
Ok(list)
}
pub fn add_annotation(
conn: &Connection,
session_id: &str,
start_time: f64,
end_time: f64,
name: &str,
description: &str,
color: &str,
) -> Result<Annotation, CommandError> {
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO annotations (id, session_id, start_time, end_time, name, description, color) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![id, session_id, start_time, end_time, name, description, color],
)?;
Ok(Annotation {
id,
session_id: session_id.to_string(),
start_time,
end_time,
name: name.to_string(),
description: description.to_string(),
color: color.to_string(),
})
}
pub fn update_annotation(conn: &Connection, annotation: &Annotation) -> Result<(), CommandError> {
conn.execute(
"UPDATE annotations SET start_time=?1, end_time=?2, name=?3, description=?4, color=?5 WHERE id=?6",
params![annotation.start_time, annotation.end_time, annotation.name, annotation.description, annotation.color, annotation.id],
)?;
Ok(())
}
pub fn delete_annotation(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM annotations WHERE id = ?1", params![id])?;
Ok(())
}

View File

@@ -0,0 +1,106 @@
use crate::error::CommandError;
use crate::models::{ChatMessage, ChatSession};
use rusqlite::{params, Connection};
use uuid::Uuid;
pub fn create_session(
conn: &Connection,
title: Option<String>,
) -> Result<ChatSession, CommandError> {
let id = Uuid::new_v4().to_string();
let now = chrono::Local::now().to_rfc3339();
let title = title.unwrap_or_else(|| "Nowy czat".to_string());
conn.execute(
"INSERT INTO chat_sessions (id, title, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
params![id, title, now, now],
)?;
Ok(ChatSession {
id,
title,
created_at: now.clone(),
updated_at: now,
})
}
pub fn get_sessions(conn: &Connection) -> Result<Vec<ChatSession>, CommandError> {
let mut stmt = conn.prepare(
"SELECT id, title, created_at, updated_at FROM chat_sessions ORDER BY updated_at DESC",
)?;
let rows = stmt.query_map([], |row| {
Ok(ChatSession {
id: row.get(0)?,
title: row.get(1)?,
created_at: row.get(2)?,
updated_at: row.get(3)?,
})
})?;
let mut sessions = Vec::new();
for r in rows {
sessions.push(r?);
}
Ok(sessions)
}
pub fn update_title(conn: &Connection, id: &str, title: &str) -> Result<(), CommandError> {
conn.execute(
"UPDATE chat_sessions SET title = ?1 WHERE id = ?2",
params![title, id],
)?;
Ok(())
}
pub fn delete_session(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM chat_sessions WHERE id = ?1", params![id])?;
Ok(())
}
pub fn save_message(
conn: &Connection,
session_id: &str,
role: &str,
content: &str,
) -> Result<ChatMessage, CommandError> {
let id = Uuid::new_v4().to_string();
let now = chrono::Local::now().to_rfc3339();
conn.execute(
"INSERT INTO chat_messages (id, session_id, role, content, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
params![id, session_id, role, content, now],
)?;
// Update session updated_at
conn.execute(
"UPDATE chat_sessions SET updated_at = ?1 WHERE id = ?2",
params![now, session_id],
)?;
Ok(ChatMessage {
id,
session_id: session_id.to_string(),
role: role.to_string(),
content: content.to_string(),
created_at: now,
})
}
pub fn get_history(conn: &Connection, session_id: &str) -> Result<Vec<ChatMessage>, CommandError> {
let mut stmt = conn.prepare("SELECT id, session_id, role, content, created_at FROM chat_messages WHERE session_id = ?1 ORDER BY created_at ASC")?;
let rows = stmt.query_map(params![session_id], |row| {
Ok(ChatMessage {
id: row.get(0)?,
session_id: row.get(1)?,
role: row.get(2)?,
content: row.get(3)?,
created_at: row.get(4)?,
})
})?;
let mut messages = Vec::new();
for r in rows {
messages.push(r?);
}
Ok(messages)
}

View File

@@ -0,0 +1,3 @@
pub mod analysis;
pub mod chat;
pub mod programs;

View File

@@ -0,0 +1,240 @@
use crate::error::CommandError;
use crate::models::{Program, Week, Workout};
use rusqlite::{params, Connection};
use uuid::Uuid;
// --- Programs ---
pub fn get_programs(conn: &Connection) -> Result<Vec<Program>, CommandError> {
let mut stmt = conn.prepare("SELECT id, name, created_at FROM programs")?;
let rows = stmt.query_map([], |row| {
Ok(Program {
id: row.get(0)?,
name: row.get(1)?,
created_at: row.get(2)?,
})
})?;
let mut programs = Vec::new();
for r in rows {
programs.push(r?);
}
Ok(programs)
}
pub fn create_program(conn: &Connection, name: &str) -> Result<Program, CommandError> {
let id = Uuid::new_v4().to_string();
let created_at = chrono::Local::now().to_rfc3339();
conn.execute(
"INSERT INTO programs (id, name, created_at) VALUES (?1, ?2, ?3)",
params![id, name, created_at],
)?;
Ok(Program {
id,
name: name.to_string(),
created_at,
})
}
pub fn delete_program(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM programs WHERE id = ?1", params![id])?;
Ok(())
}
pub fn duplicate_program(conn: &mut Connection, id: &str) -> Result<Program, CommandError> {
let tx = conn.transaction()?;
// 1. Get original program
let mut stmt = tx.prepare("SELECT name FROM programs WHERE id = ?1")?;
let original_name: String = stmt.query_row(params![id], |row| row.get(0))?;
drop(stmt); // Close statement to release borrow
// 2. Create new program
let new_program_id = Uuid::new_v4().to_string();
let new_name = format!("{} (Kopia)", original_name);
let created_at = chrono::Local::now().to_rfc3339();
tx.execute(
"INSERT INTO programs (id, name, created_at) VALUES (?1, ?2, ?3)",
params![new_program_id, new_name, created_at],
)?;
// 3. Get original weeks
let mut stmt_weeks =
tx.prepare("SELECT id, position, notes FROM program_weeks WHERE program_id = ?1")?;
let weeks_rows = stmt_weeks.query_map(params![id], |row| {
Ok((
row.get::<_, String>(0)?, // id
row.get::<_, i32>(1)?, // position
row.get::<_, String>(2)?, // notes
))
})?;
// Collect into vec to avoid borrow issues with stmt_weeks while inserting
let mut weeks_data = Vec::new();
for w in weeks_rows {
weeks_data.push(w?);
}
drop(stmt_weeks);
// 4. Clone weeks and their workouts
for (old_week_id, position, notes) in weeks_data {
let new_week_id = Uuid::new_v4().to_string();
tx.execute(
"INSERT INTO program_weeks (id, program_id, position, notes) VALUES (?1, ?2, ?3, ?4)",
params![new_week_id, new_program_id, position, notes],
)?;
// Clone workouts for this week
let mut stmt_workouts = tx.prepare(
"SELECT day, type, ref_id, name, description FROM program_workouts WHERE week_id = ?1",
)?;
let workouts_rows = stmt_workouts.query_map(params![old_week_id], |row| {
Ok((
row.get::<_, String>(0)?, // day
row.get::<_, String>(1)?, // type
row.get::<_, Option<String>>(2)?, // ref_id
row.get::<_, String>(3)?, // name
row.get::<_, String>(4)?, // description
))
})?;
let mut workouts_data = Vec::new();
for wo in workouts_rows {
workouts_data.push(wo?);
}
drop(stmt_workouts);
for (day, type_, ref_id, name, description) in workouts_data {
let new_workout_id = Uuid::new_v4().to_string();
tx.execute(
"INSERT INTO program_workouts (id, week_id, program_id, day, type, ref_id, name, description, completed) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
params![new_workout_id, new_week_id, new_program_id, day, type_, ref_id, name, description],
)?;
}
}
tx.commit()?;
Ok(Program {
id: new_program_id,
name: new_name,
created_at,
})
}
// --- Weeks ---
pub fn get_weeks(conn: &Connection, program_id: &str) -> Result<Vec<Week>, CommandError> {
let mut stmt = conn.prepare("SELECT id, program_id, position, notes FROM program_weeks WHERE program_id = ?1 ORDER BY position ASC")?;
let rows = stmt.query_map(params![program_id], |row| {
Ok(Week {
id: row.get(0)?,
program_id: row.get(1)?,
position: row.get(2)?,
notes: row.get(3).unwrap_or_default(),
})
})?;
let mut weeks = Vec::new();
for r in rows {
weeks.push(r?);
}
Ok(weeks)
}
pub fn add_week(conn: &Connection, program_id: &str, position: i32) -> Result<Week, CommandError> {
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO program_weeks (id, program_id, position, notes) VALUES (?1, ?2, ?3, ?4)",
params![id, program_id, position, ""],
)?;
Ok(Week {
id,
program_id: program_id.to_string(),
position,
notes: "".to_string(),
})
}
pub fn update_week_note(conn: &Connection, id: &str, notes: &str) -> Result<(), CommandError> {
conn.execute(
"UPDATE program_weeks SET notes = ?1 WHERE id = ?2",
params![notes, id],
)?;
Ok(())
}
pub fn delete_week(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM program_weeks WHERE id = ?1", params![id])?;
Ok(())
}
// --- Workouts ---
pub fn get_workouts(conn: &Connection, program_id: &str) -> Result<Vec<Workout>, CommandError> {
let mut stmt = conn.prepare("SELECT id, week_id, program_id, day, type, ref_id, name, description, completed FROM program_workouts WHERE program_id = ?1")?;
let rows = stmt.query_map(params![program_id], |row| {
Ok(Workout {
id: row.get(0)?,
week_id: row.get(1)?,
program_id: row.get(2)?,
day: row.get(3)?,
type_: row.get(4)?,
ref_id: row.get(5)?,
name: row.get(6)?,
description: row.get(7)?,
completed: row.get(8)?,
})
})?;
let mut workouts = Vec::new();
for r in rows {
workouts.push(r?);
}
Ok(workouts)
}
pub fn add_workout(
conn: &Connection,
week_id: &str,
program_id: &str,
day: &str,
type_: &str,
ref_id: Option<String>,
name: &str,
description: &str,
) -> Result<Workout, CommandError> {
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO program_workouts (id, week_id, program_id, day, type, ref_id, name, description, completed) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
params![id, week_id, program_id, day, type_, ref_id, name, description],
)?;
Ok(Workout {
id,
week_id: week_id.to_string(),
program_id: program_id.to_string(),
day: day.to_string(),
type_: type_.to_string(),
ref_id,
name: name.to_string(),
description: description.to_string(),
completed: false,
})
}
pub fn update_workout(conn: &Connection, workout: &Workout) -> Result<(), CommandError> {
conn.execute(
"UPDATE program_workouts SET day = ?1, week_id = ?2, type = ?3, ref_id = ?4, name = ?5, description = ?6, completed = ?7 WHERE id = ?8",
params![workout.day, workout.week_id, workout.type_, workout.ref_id, workout.name, workout.description, workout.completed, workout.id],
)?;
Ok(())
}
pub fn delete_workout(conn: &Connection, id: &str) -> Result<(), CommandError> {
conn.execute("DELETE FROM program_workouts WHERE id = ?1", params![id])?;
Ok(())
}

33
src-tauri/src/error.rs Normal file
View File

@@ -0,0 +1,33 @@
use serde::Serialize;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CommandError {
#[error("Database error: {0}")]
Db(#[from] rusqlite::Error),
#[error("Pool error: {0}")]
Pool(#[from] r2d2::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Internal error: {0}")]
Internal(String),
}
impl Serialize for CommandError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
// Helper to convert generic strings to CommandError
impl From<String> for CommandError {
fn from(s: String) -> Self {
CommandError::Internal(s)
}
}

65
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,65 @@
mod commands;
mod db;
mod engine;
mod error;
mod models;
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
// Database init
let pool = db::init_db(app.handle())?;
app.manage(pool);
Ok(())
})
.invoke_handler(tauri::generate_handler![
// Exercises
commands::exercises::get_exercises,
commands::exercises::add_exercise,
commands::exercises::update_exercise,
commands::exercises::delete_exercise,
// Plans
commands::plans::get_plans,
commands::plans::create_plan,
commands::plans::update_plan,
commands::plans::delete_plan,
// Programs
commands::programs::get_programs,
commands::programs::create_program,
commands::programs::duplicate_program,
commands::programs::delete_program,
commands::programs::get_weeks,
commands::programs::add_week,
commands::programs::update_week_note,
commands::programs::delete_week,
commands::programs::get_workouts,
commands::programs::add_workout,
commands::programs::update_workout,
commands::programs::delete_workout,
// Analysis
commands::analysis::get_analysis_sessions,
commands::analysis::create_analysis_session,
commands::analysis::delete_analysis_session,
commands::analysis::get_annotations,
commands::analysis::add_annotation,
commands::analysis::update_annotation,
commands::analysis::delete_annotation,
commands::analysis::pick_video_file, // Video Picker
// Chat
commands::chat::create_chat_session,
commands::chat::get_chat_sessions,
commands::chat::update_chat_title,
commands::chat::delete_chat_session,
commands::chat::save_chat_message,
commands::chat::get_chat_history
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
trainhubapp_lib::run()
}

110
src-tauri/src/models.rs Normal file
View File

@@ -0,0 +1,110 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Exercise {
pub id: String,
pub name: String,
pub instructions: String,
pub enrichment: String,
pub tags: String, // stored as JSON array string
pub video_url: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrainingPlan {
pub id: String,
pub name: String,
pub sections: String, // stored as JSON String
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Program {
pub id: String,
pub name: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Week {
pub id: String,
pub program_id: String,
pub position: i32,
pub notes: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Workout {
pub id: String,
pub week_id: String,
pub program_id: String,
pub day: String,
#[serde(rename = "type")]
pub type_: String, // "exercise" | "plan" (mapped from 'type' in DB)
pub ref_id: Option<String>,
pub name: String,
pub description: String,
pub completed: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisSession {
pub id: String,
pub name: String,
pub date: String,
pub video_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Annotation {
pub id: String,
pub session_id: String,
pub start_time: f64,
pub end_time: f64,
pub name: String,
pub description: String,
pub color: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Flashcard {
pub id: String,
pub set_id: String,
pub front: String,
pub back: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlashcardSet {
pub id: String,
pub name: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatSession {
pub id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatMessage {
pub id: String,
pub session_id: String,
pub role: String,
pub content: String,
pub created_at: String,
}