This commit is contained in:
2025-08-08 21:09:11 -04:00
parent 2f044c3d9c
commit 7bcc5d656c
38 changed files with 776 additions and 159 deletions

View File

@ -10,6 +10,7 @@
import SwiftUI
struct ListItem: View {
var systemName: String?
var title: String?
var text: String?
var subtitle: String?
@ -18,6 +19,9 @@ struct ListItem: View {
var body: some View {
HStack {
if let systemName = systemName {
Image(systemName: systemName)
}
VStack (alignment: .leading) {
if let title = title {
Text("\(title)")

View File

@ -17,7 +17,17 @@ struct ExerciseAddEditView: View {
@State var model: Exercise
@State var originalWeight: Int? = nil
@State var loadType: LoadType = .none
@State private var minutes = 0
@State private var seconds = 0
@State private var weight_tens = 0
@State private var weight = 0
@State private var reps = 0
@State private var sets = 0
var body: some View {
NavigationStack {
Form {
@ -40,23 +50,88 @@ struct ExerciseAddEditView: View {
}
Section (header: Text("Sets/Reps")) {
Stepper("Sets: \(model.sets)", value: $model.sets, in: 1...10)
Stepper("Reps: \(model.reps)", value: $model.reps, in: 1...50)
HStack (alignment: .bottom) {
VStack (alignment: .center) {
Text("Sets")
Picker("", selection: $sets) {
ForEach(0..<20) { sets in
Text("\(sets)").tag(sets)
}
}
.frame(height: 100)
.pickerStyle(.wheel)
}
VStack (alignment: .center) {
Text("Reps")
Picker("", selection: $reps) {
ForEach(0..<100) { reps in
Text("\(reps)").tag(reps)
}
}
.frame(height: 100)
.pickerStyle(.wheel)
}
}
}
.onAppear {
sets = model.sets
reps = model.reps
}
// Weight section
Section (header: Text("Weight")) {
HStack {
VStack(alignment: .center) {
Text("\(model.weight) lbs")
.font(.headline)
Section (header: Text("Load Type"), footer: Text("For bodyweight exercises choose None. For resistance or weight training selected 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)
}
Spacer()
VStack(alignment: .trailing) {
Stepper("±1", value: $model.weight, in: 0...1000)
Stepper("±5", value: $model.weight, in: 0...1000, step: 5)
}
.pickerStyle(.segmented)
}
if loadType == .weight {
Section (header: Text("Weight")) {
HStack {
HStack {
Picker("", selection: $weight_tens) {
ForEach(0..<100) { lbs in
Text("\(lbs*10)").tag(lbs*10)
}
}
.frame(height: 100)
.pickerStyle(.wheel)
Picker("", selection: $weight) {
ForEach(0..<10) { lbs in
Text("\(lbs)").tag(lbs)
}
}
.frame(height: 100)
.pickerStyle(.wheel)
Text("lbs")
}
}
}
}
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)
}
.frame(width: 130)
}
}
@ -73,6 +148,10 @@ struct ExerciseAddEditView: View {
}
.onAppear {
originalWeight = model.weight
weight_tens = model.weight / 10
weight = model.weight - weight_tens * 10
minutes = Int(model.duration.timeIntervalSince1970) / 60
seconds = Int(model.duration.timeIntervalSince1970) - minutes * 60
}
.sheet(isPresented: $showingExercisePicker) {
ExercisePickerView { exerciseNames in
@ -94,6 +173,10 @@ struct ExerciseAddEditView: View {
model.weightLastUpdated = Date()
}
}
model.duration = Date(timeIntervalSince1970: Double(minutes * 60 + seconds))
model.weight = weight_tens + weight
model.sets = sets
model.reps = reps
try? modelContext.save()
dismiss()
}

View File

@ -16,7 +16,8 @@ struct SplitDetailView: View {
@State var split: Split
@State private var showingAddSheet: Bool = false
@State private var showingExerciseAddSheet: Bool = false
@State private var showingSplitEditSheet: Bool = false
@State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: Exercise? = nil
@State private var createdWorkout: Workout? = nil
@ -72,24 +73,29 @@ struct SplitDetailView: View {
})
Button {
showingAddSheet = true
showingExerciseAddSheet = true
} label: {
ListItem(title: "Add Exercise")
ListItem(systemName: "plus.circle", title: "Add Exercise")
}
} else {
Text("No exercises added yet.")
Button(action: { showingAddSheet.toggle() }) {
Button(action: { showingExerciseAddSheet.toggle() }) {
ListItem(title: "Add Exercise")
}
}
}
}
Button ("Delete This Split", role: .destructive) {
showingDeleteConfirmation = true
Section {
Button ("Edit") {
showingSplitEditSheet = true
}
Button ("Delete", role: .destructive) {
showingDeleteConfirmation = true
}
}
.tint(.red)
}
.navigationTitle("\(split.name)")
}
@ -122,7 +128,7 @@ struct SplitDetailView: View {
.navigationDestination(item: $createdWorkout, destination: { workout in
WorkoutLogListView(workout: workout)
})
.sheet (isPresented: $showingAddSheet) {
.sheet (isPresented: $showingExerciseAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
let splitId = split.persistentModelID
print("exerciseNames: \(exerciseNames)")
@ -164,7 +170,10 @@ struct SplitDetailView: View {
try? modelContext.save()
}, allowMultiSelect: true)
}
.sheet(item: $itemToEdit) { item in
.sheet (isPresented: $showingSplitEditSheet) {
SplitAddEditView(model: split)
}
.sheet (item: $itemToEdit) { item in
ExerciseAddEditView(model: item)
}
.confirmationDialog(

View File

@ -0,0 +1,56 @@
//
// SplitPickerView.swift
// Workouts
//
// Created by rzen on 7/25/25 at 6:24PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitListView: View {
@Environment(\.modelContext) private var modelContext
@State var splits: [Split]
@State private var allowSorting: Bool = true
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink {
SplitDetailView(split: split)
} label: {
SplitItem(
name: split.name,
color: Color.color(from: split.color),
systemImageName: split.systemImage,
exerciseCount: split.exercises?.count ?? 0
)
.overlay(dragging ? Color.white.opacity(0.8) : Color.clear)
}
}
}
.padding()
}
.onAppear(perform: loadSplits)
}
func loadSplits () {
print("Loading splits")
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
print("Loaded \(splits.count) splits")
} catch {
print("ERROR: failed to load splits \(error)")
}
}
}

View File

@ -17,54 +17,22 @@ struct SplitsView: View {
@State var splits: [Split] = []
@State private var showingAddSheet: Bool = false
@State private var allowSorting: Bool = true
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink {
SplitDetailView(split: split)
} label: {
SplitItem(
name: split.name,
color: Color.color(from: split.color),
systemImageName: split.systemImage,
exerciseCount: split.exercises?.count ?? 0
)
.overlay(dragging ? Color.white.opacity(0.8) : Color.clear)
SplitListView(splits: splits)
.navigationTitle("Splits")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.padding()
}
.navigationTitle("Splits")
.onAppear(perform: loadSplits)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
}
.sheet (isPresented: $showingAddSheet) {
SplitAddEditView(model: Split(name: "New Split"))
}
}
func loadSplits () {
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
} catch {
print("ERROR: failed to load splits \(error)")
}
}
}

View File

@ -40,7 +40,7 @@ struct WorkoutLogListView: View {
CheckboxListItem(
status: workoutLogStatus,
title: log.exerciseName,
subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
subtitle: getSubtitleText(for: log)
)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
let status = log.status ?? WorkoutStatus.notStarted
@ -200,6 +200,41 @@ struct WorkoutLogListView: View {
}
}
func getSubtitleText(for log: WorkoutLog) -> String {
let baseText = "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
if log.status == .inProgress, let currentStateIndex = log.currentStateIndex {
let currentSet = getCurrentSetNumber(stateIndex: currentStateIndex, totalSets: log.sets)
if currentSet > 0 {
return "In Progress, Set #\(currentSet)\(baseText)"
}
}
return baseText
}
func getCurrentSetNumber(stateIndex: Int, totalSets: Int) -> Int {
// Exercise states are structured as: intro(0) set1(1) rest1(2) set2(3) rest2(4) ... done
// For each set number n, set state index = 2n-1, rest state index = 2n
if stateIndex <= 0 {
return 0 // intro or invalid
}
// Check if we're in a rest state (even indices > 0)
let isRestState = stateIndex > 0 && stateIndex % 2 == 0
if isRestState {
// During rest, show the next set number
let nextSetNumber = (stateIndex / 2) + 1
return min(nextSetNumber, totalSets)
} else {
// During set, show current set number
let currentSetNumber = (stateIndex + 1) / 2
return min(currentSetNumber, totalSets)
}
}
func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
// Use a single expression predicate that works with SwiftData
print("Searching for exercise name: \(exerciseName)")

View File

@ -20,11 +20,13 @@ struct WorkoutListView: View {
@Query(sort: [SortDescriptor(\Workout.start, order: .reverse)]) var workouts: [Workout]
// @State private var showingSplitPicker = false
@State private var showingSplitPicker = false
@State private var itemToDelete: Workout? = nil
@State private var itemToEdit: Workout? = nil
@State private var splits: [Split] = []
var body: some View {
NavigationStack {
Form {
@ -60,6 +62,14 @@ struct WorkoutListView: View {
}
}
.navigationTitle("Workouts")
.onAppear (perform: loadSplits)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button ("Start New Split") {
showingSplitPicker.toggle()
}
}
}
.sheet(item: $itemToEdit) { item in
WorkoutEditView(workout: item)
}
@ -86,8 +96,14 @@ struct WorkoutListView: View {
} message: {
Text("Are you sure you want to delete this workout?")
}
// .sheet(isPresented: $showingSplitPicker) {
// SplitPickerView { split in
.sheet(isPresented: $showingSplitPicker) {
NavigationStack {
SplitListView(splits: splits)
.navigationTitle("Select a Split")
}
// { split in
// let workout = Workout(start: Date(), end: Date(), split: split)
// modelContext.insert(workout)
// if let exercises = split.exercises {
@ -106,7 +122,20 @@ struct WorkoutListView: View {
// }
// try? modelContext.save()
// }
// }
}
}
}
func loadSplits () {
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
} catch {
print("ERROR: failed to load splits \(error)")
}
}
}