Initial commit
This commit is contained in:
145
ui/workout.py
Normal file
145
ui/workout.py
Normal 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.")
|
||||
Reference in New Issue
Block a user