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

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

7
src-tauri/2 Normal file
View File

@@ -0,0 +1,7 @@
up to date, audited 97 packages in 980ms
12 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities

5956
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

35
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
[package]
name = "trainhubapp"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "trainhubapp_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.32", features = ["bundled"] }
uuid = { version = "1.10", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
rfd = "0.15"
tauri-plugin-fs = "2"
r2d2 = "0.8"
r2d2_sqlite = "0.25"
thiserror = "1"
tauri-plugin-http = "2"
tauri-plugin-shell = "2"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,78 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default",
"fs:default",
"fs:allow-applocaldata-write",
"fs:allow-applocaldata-write-recursive",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-read-recursive",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-create",
{
"identifier": "fs:scope",
"allow": [
"$VIDEO/**",
"$DESKTOP/**",
"$DOWNLOADS/**",
"$DOCUMENTS/**",
"$APP_LOCAL_DATA/**",
"**"
]
},
"shell:allow-kill",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "llama-server",
"cmd": "",
"sidecar": true,
"args": true
}
]
},
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "llama-server",
"cmd": "",
"sidecar": true,
"args": true
}
]
},
"http:default",
{
"identifier": "http:allow-fetch",
"allow": [
{
"url": "http://127.0.0.1:8080/*"
},
{
"url": "http://127.0.0.1:8081/*"
},
{
"url": "https://huggingface.co/**"
},
{
"url": "https://cdn-lfs.huggingface.co/**"
},
{
"url": "https://*.huggingface.co/**"
},
{
"url": "https://raw.githubusercontent.com/**"
}
]
}
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,66 @@
\# Kompendium Diety Ketogenicznej (Keto)
\## Czym jest dieta keto?
Dieta ketogeniczna to model żywienia o wysokiej zawartości tłuszczu, umiarkowanej ilości białka i bardzo niskiej zawartości węglowodanów. Głównym celem jest wprowadzenie organizmu w stan \*\*ketozy\*\*, w którym ciało spala tłuszcz jako główne źródło energii zamiast glukozy.
\## Główne zasady (Makroskładniki)
Standardowy rozkład makroskładników na diecie keto wygląda następująco:
\- \*\*Tłuszcze:\*\* 70-75% dziennego zapotrzebowania kalorycznego.
\- \*\*Białko:\*\* 20-25%.
\- \*\*Węglowodany:\*\* 5-10% (zazwyczaj poniżej 20-50g węglowodanów netto dziennie).
\## Co jeść na diecie keto? (Lista zakupów)
1\. \*\*Zdrowe tłuszcze:\*\* Oliwa z oliwek, olej kokosowy, masło klarowane, awokado.
2\. \*\*Mięso i ryby:\*\* Wołowina, boczek, drób, tłuste ryby (łosoś, makrela).
3\. \*\*Nabiał:\*\* Tłuste sery, śmietana 30%, jajka.
4\. \*\*Warzywa niskowęglowodanowe:\*\* Szpinak, jarmuż, brokuły, kalafior, cukinia.
5\. \*\*Orzechy i nasiona:\*\* Orzechy włoskie, makadamia, nasiona chia, pestki dyni.
\## Produkty zakazane
\- Cukier i słodycze.
\- Produkty mączne (chleb, makaron, ciasta).
\- Owoce o wysokiej zawartości cukru (banany, winogrona, jabłka).
\- Warzywa skrobiowe (ziemniaki, bataty).
\- Rośliny strączkowe (fasola, groch).
\## Korzyści zdrowotne
\- Szybsza utrata tkanki tłuszczowej.
\- Stabilizacja poziomu cukru we krwi i insuliny.
\- Poprawa koncentracji i jasności umysłu.
\- Zmniejszenie uczucia głodu.
\## Ważna uwaga
Przed rozpoczęciem diety ketogenicznej zaleca się konsultację z lekarzem lub dietetykiem, szczególnie w przypadku osób z chorobami nerek, wątroby lub cukrzycą typu 1.

View File

@@ -0,0 +1 @@
[]

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

50
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,50 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "trainhubapp",
"version": "0.1.0",
"identifier": "pl.kciolek.trainhub",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"label": "main",
"title": "trainhubapp",
"maximized": true,
"width": 800,
"height": 600,
"visible": true
}
],
"security": {
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' asset: http://asset.localhost https://asset.localhost http://ipc.localhost http://localhost:1420 ws://localhost:1420 blob: data: https://huggingface.co https://cdn-lfs.huggingface.co https://*.huggingface.co https://raw.githubusercontent.com http://127.0.0.1:8080; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' http://ipc.localhost http://localhost:1420 ws://localhost:1420 asset: http://asset.localhost https://asset.localhost https://huggingface.co https://cdn-lfs.huggingface.co https://*.huggingface.co https://raw.githubusercontent.com https://cas-bridge.xethub.hf.co https://*.hf.co http://127.0.0.1:8080",
"assetProtocol": {
"enable": true,
"scope": [
"**"
]
}
}
},
"bundle": {
"active": true,
"targets": "all",
"externalBin": [
"llama-server"
],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"*.dll"
]
}
}