Initial commit

This commit is contained in:
2026-01-10 16:46:49 +01:00
commit 4ae02411aa
18 changed files with 1066 additions and 0 deletions

72
ui/app.py Normal file
View File

@@ -0,0 +1,72 @@
import customtkinter as ctk
from database import Database
from datetime import date
from tkinter import messagebox
class ExerciseDiaryApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Exercise Diary - Level Sub-0")
self.geometry("1000x700")
self.db = Database()
self.current_date = date.today().isoformat()
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# Sidebar
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
self.sidebar_frame.grid_rowconfigure(5, weight=1)
self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="My Diary", font=ctk.CTkFont(size=20, weight="bold"))
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
self.btn_dashboard = ctk.CTkButton(self.sidebar_frame, text="Dashboard", command=self.show_dashboard)
self.btn_dashboard.grid(row=1, column=0, padx=20, pady=10)
self.btn_daily = ctk.CTkButton(self.sidebar_frame, text="Daily Log", command=self.show_daily_log)
self.btn_daily.grid(row=2, column=0, padx=20, pady=10)
self.btn_workout = ctk.CTkButton(self.sidebar_frame, text="Workouts", command=self.show_workout_log)
self.btn_workout.grid(row=3, column=0, padx=20, pady=10)
self.btn_stats = ctk.CTkButton(self.sidebar_frame, text="Progress", command=self.show_stats)
self.btn_stats.grid(row=4, column=0, padx=20, pady=10)
# Main Content Area
self.main_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
self.main_frame.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
# Default View
self.show_dashboard()
def clear_main_frame(self):
for widget in self.main_frame.winfo_children():
widget.destroy()
def show_dashboard(self):
self.clear_main_frame()
from ui.dashboard import DashboardFrame
frame = DashboardFrame(self.main_frame, self.db)
frame.pack(fill="both", expand=True)
def show_daily_log(self):
self.clear_main_frame()
from ui.daily_log import DailyLogFrame
frame = DailyLogFrame(self.main_frame, self.db, self.current_date)
frame.pack(fill="both", expand=True)
def show_workout_log(self):
self.clear_main_frame()
from ui.workout import WorkoutFrame
frame = WorkoutFrame(self.main_frame, self.db, self.current_date)
frame.pack(fill="both", expand=True)
def show_stats(self):
self.clear_main_frame()
from ui.stats import StatsFrame
frame = StatsFrame(self.main_frame, self.db)
frame.pack(fill="both", expand=True)

122
ui/daily_log.py Normal file
View File

@@ -0,0 +1,122 @@
import customtkinter as ctk
from database import Database
from tkinter import messagebox
from utils import parse_float, format_float
class DailyLogFrame(ctk.CTkFrame):
def __init__(self, master, db: Database, date_str: str):
super().__init__(master)
self.db = db
self.date_str = date_str
self.setup_ui()
self.load_data()
def setup_ui(self):
self.grid_columnconfigure((0, 1), weight=1)
# Header
self.header = ctk.CTkLabel(self, text=f"Daily Log: {self.date_str}", font=ctk.CTkFont(size=20, weight="bold"))
self.header.grid(row=0, column=0, columnspan=2, pady=10, sticky="w")
# Sleep Section
self.sleep_frame = ctk.CTkFrame(self)
self.sleep_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=10)
ctk.CTkLabel(self.sleep_frame, text="Sleep Duration (Hours):").grid(row=0, column=0, padx=10, pady=10)
self.sleep_entry = ctk.CTkEntry(self.sleep_frame, placeholder_text="e.g. 7,5")
self.sleep_entry.grid(row=0, column=1, padx=10, pady=10)
ctk.CTkLabel(self.sleep_frame, text="Sleep Quality (1-10):").grid(row=0, column=2, padx=10, pady=10)
self.sleep_quality_slider = ctk.CTkSlider(self.sleep_frame, from_=1, to=10, number_of_steps=9)
self.sleep_quality_slider.grid(row=0, column=3, padx=10, pady=10)
self.sleep_quality_label = ctk.CTkLabel(self.sleep_frame, text="5")
self.sleep_quality_label.grid(row=0, column=4, padx=5)
self.sleep_quality_slider.configure(command=lambda val: self.sleep_quality_label.configure(text=str(int(val))))
# Body Stats Section
self.body_frame = ctk.CTkFrame(self)
self.body_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=10)
ctk.CTkLabel(self.body_frame, text="Weight (kg):").grid(row=0, column=0, padx=10, pady=10)
self.weight_entry = ctk.CTkEntry(self.body_frame, placeholder_text="0,0")
self.weight_entry.grid(row=0, column=1, padx=10, pady=10)
ctk.CTkLabel(self.body_frame, text="Waist (cm):").grid(row=0, column=2, padx=10, pady=10)
self.waist_entry = ctk.CTkEntry(self.body_frame, placeholder_text="0,0")
self.waist_entry.grid(row=0, column=3, padx=10, pady=10)
# Notes / Mood
self.notes_frame = ctk.CTkFrame(self)
self.notes_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10)
ctk.CTkLabel(self.notes_frame, text="Mood/Energy (1-10):").grid(row=0, column=0, padx=10, pady=10)
self.mood_slider = ctk.CTkSlider(self.notes_frame, from_=1, to=10, number_of_steps=9)
self.mood_slider.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
self.mood_label = ctk.CTkLabel(self.notes_frame, text="5")
self.mood_label.grid(row=0, column=2, padx=5)
self.mood_slider.configure(command=lambda val: self.mood_label.configure(text=str(int(val))))
ctk.CTkLabel(self.notes_frame, text="Notes:").grid(row=1, column=0, padx=10, pady=10, sticky="n")
self.notes_entry = ctk.CTkTextbox(self.notes_frame, height=100)
self.notes_entry.grid(row=1, column=1, columnspan=2, padx=10, pady=10, sticky="ew")
# Save Button
self.save_btn = ctk.CTkButton(self, text="Save Daily Log", command=self.save_log)
self.save_btn.grid(row=4, column=0, columnspan=2, pady=20)
def load_data(self):
log = self.db.get_daily_log(self.date_str)
if log:
if log['sleep_hours'] is not None: self.sleep_entry.insert(0, format_float(log['sleep_hours']))
if log['sleep_quality']:
self.sleep_quality_slider.set(log['sleep_quality'])
self.sleep_quality_label.configure(text=str(log['sleep_quality']))
if log['weight'] is not None: self.weight_entry.insert(0, format_float(log['weight']))
if log['waist'] is not None: self.waist_entry.insert(0, format_float(log['waist']))
if log['mood']:
self.mood_slider.set(log['mood'])
self.mood_label.configure(text=str(log['mood']))
if log['notes']: self.notes_entry.insert("0.0", log['notes'])
def save_log(self):
# We allow entries even if some fields are empty (parse_float handles this)
# However, if they typed something invalid, we should catch it
s_val = self.sleep_entry.get()
w_val = self.weight_entry.get()
wa_val = self.waist_entry.get()
sleep = parse_float(s_val) if s_val else None
weight = parse_float(w_val) if w_val else None
waist = parse_float(wa_val) if wa_val else None
# Validation check: if they entered text but it parsed to None
if (s_val and sleep is None) or (w_val and weight is None) or (wa_val and waist is None):
messagebox.showerror("Error", "Please enter valid numbers (e.g. 75.5 or 75,5)")
return
data = {
'sleep_hours': sleep,
'sleep_quality': int(self.sleep_quality_slider.get()),
'weight': weight,
'waist': waist,
'mood': int(self.mood_slider.get()),
'notes': self.notes_entry.get("0.0", "end").strip()
}
self.db.save_daily_log(self.date_str, data)
self.save_btn.configure(text="Saved!", fg_color="green")
# Cancel previous timer if exists
if hasattr(self, '_reset_btn_id'):
self.after_cancel(self._reset_btn_id)
self._reset_btn_id = self.after(2000, self.reset_save_button)
def reset_save_button(self):
if self.winfo_exists():
self.save_btn.configure(text="Save Daily Log", fg_color=["#3B8ED0", "#1F6AA5"])
def destroy(self):
if hasattr(self, '_reset_btn_id'):
self.after_cancel(self._reset_btn_id)
super().destroy()

186
ui/dashboard.py Normal file
View File

@@ -0,0 +1,186 @@
import customtkinter as ctk
from database import Database
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import datetime
import numpy as np
from utils import parse_float, get_bmi_status, get_bmi_icon
class DashboardFrame(ctk.CTkFrame):
def __init__(self, master, db: Database):
super().__init__(master)
self.db = db
self.profile = self.db.get_user_profile()
self.setup_ui()
def setup_ui(self):
self.clear_ui()
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
# Header
self.header = ctk.CTkLabel(self, text="Status & Prognosis", font=ctk.CTkFont(size=24, weight="bold"))
self.header.grid(row=0, column=0, pady=20, sticky="n")
# Check if profile exists
if not self.profile:
self.show_setup_form()
else:
self.show_dashboard()
def clear_ui(self):
for widget in self.winfo_children():
widget.destroy()
def show_setup_form(self):
frame = ctk.CTkFrame(self)
frame.grid(row=1, column=0, pady=20)
ctk.CTkLabel(frame, text="Welcome! Let's set up your profile.", font=ctk.CTkFont(size=16)).pack(pady=10)
ctk.CTkLabel(frame, text="Height (cm):").pack(pady=5)
self.height_entry = ctk.CTkEntry(frame, placeholder_text="e.g. 180,0")
self.height_entry.pack(pady=5)
ctk.CTkLabel(frame, text="Goal Weight (kg):").pack(pady=5)
self.goal_entry = ctk.CTkEntry(frame, placeholder_text="e.g. 75,0")
self.goal_entry.pack(pady=5)
ctk.CTkButton(frame, text="Save Profile", command=self.save_profile).pack(pady=20)
def save_profile(self):
h = parse_float(self.height_entry.get())
g = parse_float(self.goal_entry.get())
if h is not None and g is not None:
self.db.save_user_profile(h, g)
self.profile = self.db.get_user_profile()
self.setup_ui()
else:
ctk.CTkLabel(self, text="Invalid input. Use numbers (e.g. 180.5 or 180,5).", text_color="red").grid(row=2, column=0)
def show_dashboard(self):
# 1. Fetch Data
logs = self.db.get_log_history(90) # Last 90 days for trends
logs.reverse() # Oldest first
latest_log = logs[-1] if logs else None
current_weight = latest_log['weight'] if latest_log and latest_log['weight'] else None
# --- Top Cards: BMI & Goal ---
cards_frame = ctk.CTkFrame(self, fg_color="transparent")
cards_frame.grid(row=1, column=0, sticky="ew", padx=20)
cards_frame.grid_columnconfigure((0, 1, 2), weight=1)
# BMI Card
bmi_frame = ctk.CTkFrame(cards_frame)
bmi_frame.grid(row=0, column=0, padx=10, sticky="ew")
bmi_val_str = "N/A"
bmi_status_text = ""
bmi_color = "white"
bmi_icon_img = None
if current_weight:
height_m = self.profile['height_cm'] / 100
bmi = current_weight / (height_m ** 2)
bmi_val_str = f"{bmi:.1f}"
bmi_status_text, _, bmi_color = get_bmi_status(bmi)
bmi_icon_img = get_bmi_icon(bmi, size=(80, 80))
# Layout BMI Card
# Left: Icon, Right: Text
bmi_inner = ctk.CTkFrame(bmi_frame, fg_color="transparent")
bmi_inner.pack(pady=10, padx=10)
if bmi_icon_img:
icon_label = ctk.CTkLabel(bmi_inner, text="", image=bmi_icon_img)
icon_label.pack(side="left", padx=(0, 15))
text_container = ctk.CTkFrame(bmi_inner, fg_color="transparent")
text_container.pack(side="left")
ctk.CTkLabel(text_container, text="BMI", font=ctk.CTkFont(size=14)).pack(anchor="w")
ctk.CTkLabel(text_container, text=bmi_val_str, font=ctk.CTkFont(size=30, weight="bold"), text_color=bmi_color).pack(anchor="w")
ctk.CTkLabel(text_container, text=bmi_status_text, font=ctk.CTkFont(size=12)).pack(anchor="w")
# Weight Card
weight_frame = ctk.CTkFrame(cards_frame)
weight_frame.grid(row=0, column=1, padx=10, sticky="ew")
w_text = f"{current_weight} kg" if current_weight else "-- kg"
g_text = f"Goal: {self.profile['goal_weight_kg']} kg"
ctk.CTkLabel(weight_frame, text="Current Weight", font=ctk.CTkFont(size=14)).pack(pady=(10,0))
ctk.CTkLabel(weight_frame, text=w_text, font=ctk.CTkFont(size=30, weight="bold")).pack()
ctk.CTkLabel(weight_frame, text=g_text, font=ctk.CTkFont(size=12, slant="italic")).pack(pady=(0,10))
# Date Estimate Card
date_frame = ctk.CTkFrame(cards_frame)
date_frame.grid(row=0, column=2, padx=10, sticky="ew")
estimated_date_str = "-1 (Insuff. Data)"
# Calculate Projection
valid_logs = [l for l in logs if l['weight'] is not None]
if len(valid_logs) >= 3:
dates_num = [(datetime.datetime.strptime(l['date'], "%Y-%m-%d") - datetime.datetime.strptime(valid_logs[0]['date'], "%Y-%m-%d")).days for l in valid_logs]
weights = [l['weight'] for l in valid_logs]
# Linear Regression
slope, intercept = np.polyfit(dates_num, weights, 1)
# Time to goal
goal = self.profile['goal_weight_kg']
if slope < 0 and current_weight > goal: # Losing weight towards goal
days_to_goal = (goal - intercept) / slope
target_date = datetime.datetime.strptime(valid_logs[0]['date'], "%Y-%m-%d") + datetime.timedelta(days=days_to_goal)
estimated_date_str = target_date.strftime("%b %d, %Y")
elif slope > 0 and current_weight < goal: # Gaining weight towards goal
days_to_goal = (goal - intercept) / slope
target_date = datetime.datetime.strptime(valid_logs[0]['date'], "%Y-%m-%d") + datetime.timedelta(days=days_to_goal)
estimated_date_str = target_date.strftime("%b %d, %Y")
else:
estimated_date_str = "Trend mismatch" # Not moving towards goal
ctk.CTkLabel(date_frame, text="Est. Goal Date", font=ctk.CTkFont(size=14)).pack(pady=(10,0))
ctk.CTkLabel(date_frame, text=estimated_date_str, font=ctk.CTkFont(size=20, weight="bold")).pack(pady=5)
ctk.CTkLabel(date_frame, text="Based on recent trend", font=ctk.CTkFont(size=10)).pack(pady=(0,10))
# --- Graph ---
self.graph_frame = ctk.CTkFrame(self)
self.graph_frame.grid(row=2, column=0, sticky="nsew", padx=20, pady=20)
self.plot_prognosis(valid_logs if 'valid_logs' in locals() else [])
def plot_prognosis(self, logs):
if not logs: return
dates = [datetime.datetime.strptime(l['date'], "%Y-%m-%d") for l in logs]
weights = [l['weight'] for l in logs]
fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
fig.patch.set_facecolor('#2b2b2b')
ax.set_facecolor('#2b2b2b')
# Plot History
ax.plot(dates, weights, color='tab:blue', marker='o', label='History')
# Plot Goal Line
ax.axhline(y=self.profile['goal_weight_kg'], color='tab:green', linestyle=':', label='Goal')
ax.set_ylabel('Weight (kg)', color='white')
ax.tick_params(axis='x', rotation=45, colors='white')
ax.tick_params(axis='y', colors='white')
ax.spines['bottom'].set_color('white')
ax.spines['top'].set_color('none')
ax.spines['left'].set_color('white')
ax.spines['right'].set_color('none')
fig.tight_layout()
canvas = FigureCanvasTkAgg(fig, master=self.graph_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)

99
ui/stats.py Normal file
View File

@@ -0,0 +1,99 @@
import customtkinter as ctk
from database import Database
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import datetime
class StatsFrame(ctk.CTkFrame):
def __init__(self, master, db: Database):
super().__init__(master)
self.db = db
self.setup_ui()
def setup_ui(self):
self.grid_columnconfigure(0, weight=1)
self.header = ctk.CTkLabel(self, text="Progress & Motivation", font=ctk.CTkFont(size=20, weight="bold"))
self.header.grid(row=0, column=0, pady=10, sticky="w")
# Stats Summary (Streaks, etc.)
self.summary_frame = ctk.CTkFrame(self)
self.summary_frame.grid(row=1, column=0, sticky="ew", pady=10)
logs = self.db.get_log_history(30)
streak = self.calculate_streak(logs)
self.streak_label = ctk.CTkLabel(self.summary_frame, text=f"🔥 Current Streak: {streak} Days", font=ctk.CTkFont(size=16))
self.streak_label.pack(side="left", padx=20, pady=10)
# Chart Section
self.chart_frame = ctk.CTkFrame(self)
self.chart_frame.grid(row=2, column=0, sticky="nsew", pady=10)
self.chart_frame.grid_columnconfigure(0, weight=1)
self.plot_weight_waist(logs)
def calculate_streak(self, logs):
if not logs: return 0
streak = 0
today = datetime.date.today()
# Simple streak calculation based on existing logs
# Checks if logs are consecutive
log_dates = [datetime.datetime.strptime(l['date'], "%Y-%m-%d").date() for l in logs]
log_dates.sort(reverse=True)
current = today
if log_dates and log_dates[0] < today - datetime.timedelta(days=1):
return 0 # Missed yesterday and today
for d in log_dates:
if d == current or d == current - datetime.timedelta(days=1):
streak += 1
current = d
else:
break
return streak
def plot_weight_waist(self, logs):
if not logs or len(logs) < 2:
ctk.CTkLabel(self.chart_frame, text="Log data for at least 2 days to see charts.").pack(pady=50)
return
# Prepare data
logs.reverse() # Chronological
dates = [l['date'][5:] for l in logs if l['weight'] or l['waist']] # MM-DD
weights = [l['weight'] for l in logs if l['weight'] or l['waist']]
waists = [l['waist'] for l in logs if l['weight'] or l['waist']]
if not weights: return
# Create plot
fig, ax1 = plt.subplots(figsize=(6, 4), dpi=100)
fig.patch.set_facecolor('#2b2b2b') # Matches dark mode
ax1.set_facecolor('#2b2b2b')
color = 'tab:blue'
ax1.set_xlabel('Date')
ax1.set_ylabel('Weight (kg)', color=color)
ax1.plot(dates, weights, color=color, marker='o', label='Weight')
ax1.tick_params(axis='y', labelcolor=color)
ax1.tick_params(axis='x', rotation=45, colors='white')
ax1.spines['bottom'].set_color('white')
ax1.spines['top'].set_color('none')
ax1.spines['left'].set_color('white')
ax1.spines['right'].set_color('none')
# Second axis for Waist
ax2 = ax1.twinx()
color = 'tab:red'
ax2.set_ylabel('Waist (cm)', color=color)
ax2.plot(dates, waists, color=color, marker='x', linestyle='--', label='Waist')
ax2.tick_params(axis='y', labelcolor=color)
ax2.spines['right'].set_color('white')
fig.tight_layout()
canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)

145
ui/workout.py Normal file
View File

@@ -0,0 +1,145 @@
import customtkinter as ctk
from database import Database
from datetime import date
from tkinter import messagebox
from utils import parse_float
class WorkoutFrame(ctk.CTkFrame):
def __init__(self, master, db: Database, date_str: str):
super().__init__(master)
self.db = db
self.date_str = date_str
self.session_id = None
self.setup_ui()
self.refresh_workouts()
def setup_ui(self):
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
# Header
self.header_frame = ctk.CTkFrame(self, fg_color="transparent")
self.header_frame.grid(row=0, column=0, sticky="ew", pady=(0, 20))
self.header_label = ctk.CTkLabel(self.header_frame, text=f"Workouts: {self.date_str}", font=ctk.CTkFont(size=20, weight="bold"))
self.header_label.pack(side="left")
# Log New Set Section
self.input_frame = ctk.CTkFrame(self)
self.input_frame.grid(row=1, column=0, sticky="ew", pady=10, padx=5)
# Exercise Selection
ctk.CTkLabel(self.input_frame, text="Exercise:").grid(row=0, column=0, padx=10, pady=10)
self.exercises = self.db.get_all_exercises()
self.exercise_names = [e['name'] for e in self.exercises]
self.exercise_var = ctk.StringVar(value=self.exercise_names[0] if self.exercise_names else "")
self.exercise_menu = ctk.CTkOptionMenu(self.input_frame, values=self.exercise_names, variable=self.exercise_var, command=self.update_input_fields)
self.exercise_menu.grid(row=0, column=1, padx=10, pady=10)
# Variation
ctk.CTkLabel(self.input_frame, text="Variation:").grid(row=0, column=2, padx=10, pady=10)
self.variation_entry = ctk.CTkEntry(self.input_frame, placeholder_text="e.g. Knee, Incline")
self.variation_entry.grid(row=0, column=3, padx=10, pady=10)
# Reps/Time
self.reps_label = ctk.CTkLabel(self.input_frame, text="Reps:")
self.reps_label.grid(row=1, column=0, padx=10, pady=10)
self.reps_entry = ctk.CTkEntry(self.input_frame, placeholder_text="0")
self.reps_entry.grid(row=1, column=1, padx=10, pady=10)
# RPE
ctk.CTkLabel(self.input_frame, text="RPE (1-10):").grid(row=1, column=2, padx=10, pady=10)
self.rpe_slider = ctk.CTkSlider(self.input_frame, from_=1, to=10, number_of_steps=9)
self.rpe_slider.grid(row=1, column=3, padx=10, pady=10)
self.add_btn = ctk.CTkButton(self.input_frame, text="Add Set", command=self.add_set)
self.add_btn.grid(row=1, column=4, padx=20, pady=10)
# History of today's sets
self.history_label = ctk.CTkLabel(self, text="Today's Sets", font=ctk.CTkFont(size=16, weight="bold"))
self.history_label.grid(row=2, column=0, sticky="w", pady=(20, 5))
self.scrollable_frame = ctk.CTkScrollableFrame(self)
self.scrollable_frame.grid(row=3, column=0, sticky="nsew", pady=5)
self.scrollable_frame.grid_columnconfigure(0, weight=1)
def update_input_fields(self, choice):
exercise = next((e for e in self.exercises if e['name'] == choice), None)
if exercise and exercise['type'] == 'time':
self.reps_label.configure(text="Seconds:")
else:
self.reps_label.configure(text="Reps:")
def refresh_workouts(self):
# Clear scrollable frame
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
workouts = self.db.get_workouts_by_date(self.date_str)
if not workouts:
ctk.CTkLabel(self.scrollable_frame, text="No sets logged for today.").pack(pady=20)
return
for session in workouts:
for s in session.get('sets', []):
set_str = f"{s['exercise_name']}"
if s['variation']: set_str += f" ({s['variation']})"
val_type = "reps" if s['reps'] > 0 else "sec"
val = s['reps'] if s['reps'] > 0 else s['duration_seconds']
set_detail = f"{val} {val_type} | RPE: {s['rpe']}"
row = ctk.CTkFrame(self.scrollable_frame)
row.pack(fill="x", pady=2, padx=5)
# Delete Button
del_btn = ctk.CTkButton(row, text="X", width=30, fg_color="#c0392b", hover_color="#e74c3c",
command=lambda sid=s['id']: self.delete_set(sid))
del_btn.pack(side="right", padx=5, pady=5)
ctk.CTkLabel(row, text=set_detail).pack(side="right", padx=10)
ctk.CTkLabel(row, text=set_str, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10)
def delete_set(self, set_id):
self.db.delete_set(set_id)
self.refresh_workouts()
def add_set(self):
if not self.session_id:
# Create session for today if it doesn't exist
# Note: For simplicity, we create one session per day in this view
existing = self.db.get_workouts_by_date(self.date_str)
if existing:
self.session_id = existing[0]['id']
else:
self.session_id = self.db.create_workout_session(self.date_str)
exercise = next((e for e in self.exercises if e['name'] == self.exercise_var.get()), None)
if not exercise: return
val_raw = self.reps_entry.get()
val_parsed = parse_float(val_raw)
if val_parsed is None and val_raw:
messagebox.showerror("Error", "Please enter a valid number.")
return
try:
val = int(val_parsed) if val_parsed is not None else 0
reps = val if exercise['type'] == 'reps' else 0
duration = val if exercise['type'] == 'time' else 0
self.db.add_set(
session_id=self.session_id,
exercise_id=exercise['id'],
reps=reps,
duration=duration,
rpe=int(self.rpe_slider.get()),
variation=self.variation_entry.get()
)
self.reps_entry.delete(0, 'end')
self.refresh_workouts()
except (ValueError, TypeError):
messagebox.showerror("Error", "Please enter a valid number for Reps/Seconds.")