Initialize repo
7
src-tauri/.gitignore
vendored
Normal 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
@@ -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
35
src-tauri/Cargo.toml
Normal 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
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
78
src-tauri/capabilities/default.json
Normal 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
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
66
src-tauri/knowledge/Dieta Ketogeniczna.md
Normal 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.
|
||||
|
||||
1
src-tauri/knowledge_base.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
92
src-tauri/src/commands/analysis.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
56
src-tauri/src/commands/chat.rs
Normal 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)
|
||||
}
|
||||
72
src-tauri/src/commands/exercises.rs
Normal 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(())
|
||||
}
|
||||
5
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod analysis;
|
||||
pub mod chat;
|
||||
pub mod exercises;
|
||||
pub mod plans;
|
||||
pub mod programs;
|
||||
61
src-tauri/src/commands/plans.rs
Normal 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(())
|
||||
}
|
||||
112
src-tauri/src/commands/programs.rs
Normal 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, ¬es)
|
||||
}
|
||||
|
||||
#[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
@@ -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)
|
||||
}
|
||||
112
src-tauri/src/engine/analysis.rs
Normal 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(())
|
||||
}
|
||||
106
src-tauri/src/engine/chat.rs
Normal 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)
|
||||
}
|
||||
3
src-tauri/src/engine/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod analysis;
|
||||
pub mod chat;
|
||||
pub mod programs;
|
||||
240
src-tauri/src/engine/programs.rs
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||