Exercise Library/ holds per-exercise reference docs (setup, cues, mistakes, progressions) with SVG visuals and a Python-rendered motion pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json rigs as animated stick figures on the exercise screen. Exercises gain a warm-up/main-circuit category, timed exercises display hold time via planSummary, and a completed exercise reopens to a check screen instead of its timers.
228 lines
8.8 KiB
Swift
228 lines
8.8 KiB
Swift
//
|
|
// ExerciseAddEditView.swift
|
|
// Workouts
|
|
//
|
|
// Created by rzen on 7/15/25 at 7:12 AM.
|
|
//
|
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct ExerciseAddEditView: View {
|
|
@Environment(SyncEngine.self) private var sync
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var showingExercisePicker = false
|
|
|
|
// The exercise entity provides initial values (read-only).
|
|
let exercise: Exercise
|
|
// The parent split is needed to rebuild and save the SplitDocument.
|
|
let split: Split
|
|
|
|
// Local editable state
|
|
@State private var exerciseName: String
|
|
@State private var originalWeight: Int
|
|
@State private var loadType: LoadType
|
|
@State private var minutes: Int
|
|
@State private var seconds: Int
|
|
@State private var weightTens: Int
|
|
@State private var weightOnes: Int
|
|
@State private var reps: Int
|
|
@State private var sets: Int
|
|
@State private var weightReminderWeeks: Int
|
|
@State private var weightLastUpdated: Date?
|
|
@State private var category: ExerciseCategory
|
|
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
|
|
|
init(exercise: Exercise, split: Split) {
|
|
self.exercise = exercise
|
|
self.split = split
|
|
|
|
let w = exercise.weight
|
|
_exerciseName = State(initialValue: exercise.name)
|
|
_originalWeight = State(initialValue: w)
|
|
_loadType = State(initialValue: exercise.loadTypeEnum)
|
|
_minutes = State(initialValue: exercise.durationMinutes)
|
|
_seconds = State(initialValue: exercise.durationSeconds)
|
|
_weightTens = State(initialValue: (w / 10) * 10)
|
|
_weightOnes = State(initialValue: w % 10)
|
|
_reps = State(initialValue: exercise.reps)
|
|
_sets = State(initialValue: exercise.sets)
|
|
_weightReminderWeeks = State(initialValue: exercise.weightReminderTimeIntervalWeeks)
|
|
_weightLastUpdated = State(initialValue: exercise.weightLastUpdated)
|
|
_category = State(initialValue: exercise.categoryEnum)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section(header: Text("Exercise")) {
|
|
if exerciseName.isEmpty {
|
|
Button(action: {
|
|
showingExercisePicker = true
|
|
}) {
|
|
HStack {
|
|
Text("Select Exercise")
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
} else {
|
|
ListItem(title: exerciseName)
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Sets/Reps")) {
|
|
HStack(alignment: .bottom) {
|
|
VStack(alignment: .center) {
|
|
Text("Sets")
|
|
Picker("", selection: $sets) {
|
|
ForEach(0..<20) { s in
|
|
Text("\(s)").tag(s)
|
|
}
|
|
}
|
|
.frame(height: 100)
|
|
.pickerStyle(.wheel)
|
|
}
|
|
VStack(alignment: .center) {
|
|
Text("Reps")
|
|
Picker("", selection: $reps) {
|
|
ForEach(0..<100) { r in
|
|
Text("\(r)").tag(r)
|
|
}
|
|
}
|
|
.frame(height: 100)
|
|
.pickerStyle(.wheel)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(
|
|
header: Text("Category"),
|
|
footer: Text("The split screen groups its exercises by category, with the warm-up listed first.")
|
|
) {
|
|
Picker("", selection: $category) {
|
|
ForEach(ExerciseCategory.displayOrder, id: \.self) { cat in
|
|
Text(cat.displayName)
|
|
.tag(cat)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
|
|
Section(
|
|
header: Text("Load Type"),
|
|
footer: Text("For bodyweight exercises choose None. For resistance or weight training select Weight. For exercises that are time oriented (like plank or meditation) select Time.")
|
|
) {
|
|
Picker("", selection: $loadType) {
|
|
ForEach(LoadType.allCases, id: \.self) { load in
|
|
Text(load.name)
|
|
.tag(load)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
|
|
if loadType == .weight {
|
|
Section(header: Text("Weight")) {
|
|
HStack {
|
|
Picker("", selection: $weightTens) {
|
|
ForEach(0..<100) { lbs in
|
|
Text("\(lbs * 10)").tag(lbs * 10)
|
|
}
|
|
}
|
|
.frame(height: 100)
|
|
.pickerStyle(.wheel)
|
|
|
|
Picker("", selection: $weightOnes) {
|
|
ForEach(0..<10) { lbs in
|
|
Text("\(lbs)").tag(lbs)
|
|
}
|
|
}
|
|
.frame(height: 100)
|
|
.pickerStyle(.wheel)
|
|
|
|
Text(weightUnit.abbreviation)
|
|
}
|
|
}
|
|
}
|
|
|
|
if loadType == .duration {
|
|
Section(header: Text("Duration")) {
|
|
HStack {
|
|
Picker("Minutes", selection: $minutes) {
|
|
ForEach(0..<60) { minute in
|
|
Text("\(minute) min").tag(minute)
|
|
}
|
|
}
|
|
.pickerStyle(.wheel)
|
|
|
|
Picker("Seconds", selection: $seconds) {
|
|
ForEach(0..<60) { second in
|
|
Text("\(second) sec").tag(second)
|
|
}
|
|
}
|
|
.pickerStyle(.wheel)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Weight Increase")) {
|
|
HStack {
|
|
Text("Remind every \(weightReminderWeeks) weeks")
|
|
Spacer()
|
|
Stepper("", value: $weightReminderWeeks, in: 0...366)
|
|
}
|
|
if let lastUpdated = weightLastUpdated {
|
|
Text("Last weight change \(Date().humanTimeInterval(to: lastUpdated)) ago")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingExercisePicker) {
|
|
ExercisePickerView { exerciseNames in
|
|
exerciseName = exerciseNames.first ?? "Unnamed"
|
|
}
|
|
}
|
|
.navigationTitle(exerciseName.isEmpty ? "New Exercise" : exerciseName)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Cancel") {
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Save") {
|
|
saveExercise()
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func saveExercise() {
|
|
let newWeight = weightTens + weightOnes
|
|
let updatedWeightDate: Date? = newWeight != originalWeight ? Date() : weightLastUpdated
|
|
let durationSecs = minutes * 60 + seconds
|
|
|
|
var doc = SplitDocument(from: split)
|
|
if let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) {
|
|
doc.exercises[idx].name = exerciseName
|
|
doc.exercises[idx].sets = sets
|
|
doc.exercises[idx].reps = reps
|
|
doc.exercises[idx].weight = newWeight
|
|
doc.exercises[idx].loadType = loadType.rawValue
|
|
doc.exercises[idx].durationSeconds = durationSecs
|
|
doc.exercises[idx].weightLastUpdated = updatedWeightDate
|
|
doc.exercises[idx].weightReminderWeeks = weightReminderWeeks
|
|
doc.exercises[idx].category = category.rawValue
|
|
}
|
|
doc.updatedAt = Date()
|
|
Task { await sync.save(split: doc) }
|
|
}
|
|
}
|