screen-locker/screen_locker/_workout_forms.py
Krzysztof kuhy Rudnicki aaca61a830 WIP: Enforce 500-line limit - split batch 1
Split 16+ files. 27 files still need splitting. See session notes.
2026-03-16 22:46:48 +01:00

270 lines
9.9 KiB
Python

"""Workout form methods mixin for the screen locker."""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.screen_locker._constants import (
MAX_DISTANCE_KM,
MAX_PACE_MIN_PER_KM,
MAX_REPS,
MAX_SETS,
MAX_TIME_MINUTES,
MAX_WEIGHT_KG,
MIN_EXERCISE_NAME_LEN,
STRENGTH_FIELDS,
)
if TYPE_CHECKING:
import tkinter as tk
class WorkoutFormsMixin:
"""Mixin providing workout form creation and validation."""
# ------------------------------------------------------------------
# Workout type selection
# ------------------------------------------------------------------
def ask_workout_type(self) -> None:
"""Display workout type selection dialog."""
self.clear_container()
self._label("What type of workout?", pady=30)
frame = self._button_row()
self._button(
frame,
"STRENGTH",
bg="#cc6600",
command=self.ask_strength_details,
width=12,
).pack(side="left", padx=20)
# ------------------------------------------------------------------
# Running workout
# ------------------------------------------------------------------
def _create_running_entries(self) -> list[tk.Entry]:
"""Create running workout entry fields."""
self.distance_entry = self._entry_row("Distance (km):")
self.time_entry = self._entry_row("Time (minutes):")
self.pace_entry = self._entry_row("Pace (min/km):")
return [self.distance_entry, self.time_entry, self.pace_entry]
def ask_running_details(self) -> None:
"""Display running workout input form."""
self.clear_container()
self.workout_data["type"] = "running"
self._label("Running Details", pady=20)
entries = self._create_running_entries()
self._setup_form_controls(
entries,
self.verify_running_data,
self.ask_workout_type,
)
def _check_running_ranges(
self,
distance: float,
time_mins: float,
pace: float,
) -> str | None:
"""Check if running values are in valid ranges."""
if distance <= 0 or distance > MAX_DISTANCE_KM:
return f"Distance seems unrealistic (0-{MAX_DISTANCE_KM} km)"
if time_mins <= 0 or time_mins > MAX_TIME_MINUTES:
return f"Time seems unrealistic (0-{MAX_TIME_MINUTES} minutes)"
if pace <= 0 or pace > MAX_PACE_MIN_PER_KM:
return f"Pace seems unrealistic (0-{MAX_PACE_MIN_PER_KM} min/km)"
expected_pace = time_mins / distance
tolerance = expected_pace * 0.15 # 15% tolerance
if abs(pace - expected_pace) > tolerance:
return (
f"Pace doesn't match! "
f"Expected ~{expected_pace:.2f} min/km, got {pace:.2f}"
)
return None
def _validate_running_input(self) -> tuple[float, float, float] | None:
"""Parse and validate running input fields."""
try:
distance = float(self.distance_entry.get())
time_mins = float(self.time_entry.get())
pace = float(self.pace_entry.get())
except ValueError:
self.show_error("Please enter valid numbers")
return None
error = self._check_running_ranges(distance, time_mins, pace)
if error:
self.show_error(error)
return None
return distance, time_mins, pace
def verify_running_data(self) -> None:
"""Validate running workout data and unlock if valid."""
result = self._validate_running_input()
if result is None:
return
distance, time_mins, pace = result
self.workout_data["distance_km"] = str(distance)
self.workout_data["time_minutes"] = str(time_mins)
self.workout_data["pace_min_per_km"] = str(pace)
self._attempt_unlock()
# ------------------------------------------------------------------
# Strength workout
# ------------------------------------------------------------------
def _create_strength_entries(self) -> list[tk.Entry]:
"""Create strength training entry fields."""
entries = [
self._entry_row(lbl, width=w, font_size=18) for lbl, w in STRENGTH_FIELDS
]
(
self.exercises_entry,
self.sets_entry,
self.reps_entry,
self.weights_entry,
self.total_weight_entry,
) = entries
return entries
def ask_strength_details(self) -> None:
"""Display strength training input form."""
self.clear_container()
self.workout_data["type"] = "strength"
self._label("Strength Training Details", pady=20)
entries = self._create_strength_entries()
self._setup_form_controls(
entries,
self.verify_strength_data,
self.ask_workout_type,
)
def _parse_reps(self, reps_raw: list[str]) -> list[list[int]]:
"""Parse reps input - single number or variable reps like '12+11+12'."""
reps: list[list[int]] = []
for r in reps_raw:
if "+" in r:
reps.append([int(x.strip()) for x in r.split("+")])
else:
reps.append([int(r)])
return reps
def _validate_strength_inputs(
self,
exercises: list[str],
sets: list[int],
reps: list[list[int]],
weights: list[float],
) -> str | None:
"""Validate strength workout inputs. Returns error message or None."""
if not (len(exercises) == len(sets) == len(reps) == len(weights)):
return "Number of exercises, sets, reps, and weights must match"
if any(len(ex) < MIN_EXERCISE_NAME_LEN for ex in exercises):
return "Exercise names too short - be specific"
if any(s < 1 or s > MAX_SETS for s in sets):
return f"Sets should be between 1-{MAX_SETS}"
if any(w < 0 or w > MAX_WEIGHT_KG for w in weights):
return f"Weights should be between 0-{MAX_WEIGHT_KG} kg"
return self._validate_reps(exercises, sets, reps)
def _validate_reps(
self,
exercises: list[str],
sets: list[int],
reps: list[list[int]],
) -> str | None:
"""Validate reps data. Returns error message or None if valid."""
for i, rep_list in enumerate(reps):
if any(r < 1 or r > MAX_REPS for r in rep_list):
return f"Reps should be between 1-{MAX_REPS}"
if len(rep_list) > 1 and len(rep_list) != sets[i]:
return (
f"For {exercises[i]!r}: variable reps count "
f"({len(rep_list)}) doesn't match sets ({sets[i]})"
)
return None
def _calculate_expected_total(
self,
sets: list[int],
reps: list[list[int]],
weights: list[float],
) -> float:
"""Calculate expected total weight lifted."""
expected_total = 0.0
for i, rep_list in enumerate(reps):
if len(rep_list) == 1:
expected_total += sets[i] * rep_list[0] * weights[i]
else:
expected_total += sum(rep_list) * weights[i]
return expected_total
def _parse_strength_entries(
self,
) -> tuple[list[str], list[int], list[list[int]], list[float], float]:
"""Parse raw strength training input from entry widgets."""
exercises = [e.strip() for e in self.exercises_entry.get().split(",")]
sets = [int(s.strip()) for s in self.sets_entry.get().split(",")]
reps_raw = [r.strip() for r in self.reps_entry.get().split(",")]
reps = self._parse_reps(reps_raw)
weights = [float(w.strip()) for w in self.weights_entry.get().split(",")]
total_weight = float(self.total_weight_entry.get())
return exercises, sets, reps, weights, total_weight
def _check_total_weight(
self,
sets: list[int],
reps: list[list[int]],
weights: list[float],
total_weight: float,
) -> str | None:
"""Verify total weight matches individual exercise calculations."""
expected = self._calculate_expected_total(sets, reps, weights)
tolerance = expected * 0.15 # 15% tolerance
if abs(total_weight - expected) > tolerance:
return (
f"Total weight doesn't match! "
f"Expected ~{expected:.1f} kg, got {total_weight:.1f}"
)
return None
def _store_strength_data(
self,
exercises: list[str],
sets: list[int],
reps: list[list[int]],
weights: list[float],
total_weight: float,
) -> None:
"""Store validated strength workout data."""
self.workout_data["exercises"] = exercises
self.workout_data["sets"] = [str(s) for s in sets]
self.workout_data["reps"] = [
"+".join(str(r) for r in rep_list) for rep_list in reps
]
self.workout_data["weights_kg"] = [str(w) for w in weights]
self.workout_data["total_weight_kg"] = str(total_weight)
def verify_strength_data(self) -> None:
"""Validate strength workout data and unlock if valid."""
try:
self._verify_strength_data_inner()
except ValueError:
self.show_error("Please enter valid data in correct format")
def _verify_strength_data_inner(self) -> None:
"""Parse, validate, and store strength data."""
data = self._parse_strength_entries()
exercises, sets, reps, weights, total_weight = data
error = self._validate_strength_inputs(exercises, sets, reps, weights)
if error:
self.show_error(error)
return
total_err = self._check_total_weight(sets, reps, weights, total_weight)
if total_err:
self.show_error(total_err)
return
self._store_strength_data(exercises, sets, reps, weights, total_weight)
self._attempt_unlock()