initial pre-viable version of watch app
This commit is contained in:
		@@ -47,10 +47,27 @@
 | 
			
		||||
		A45FA0A12E21B3DE00581607 /* Exceptions for "Workouts" folder in "Workouts" target */ = {
 | 
			
		||||
			isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
 | 
			
		||||
			membershipExceptions = (
 | 
			
		||||
				_ATTIC_/ContentView_backup.swift,
 | 
			
		||||
				_ATTIC_/ExerciseProgressView_backup.swift,
 | 
			
		||||
				Info.plist,
 | 
			
		||||
			);
 | 
			
		||||
			target = A45FA0902E21B3DD00581607 /* Workouts */;
 | 
			
		||||
		};
 | 
			
		||||
		A45FA2C52E2D3CBD00581607 /* Exceptions for "Workouts" folder in "Worksouts Watch App" target */ = {
 | 
			
		||||
			isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
 | 
			
		||||
			membershipExceptions = (
 | 
			
		||||
				_ATTIC_/ContentView_backup.swift,
 | 
			
		||||
				_ATTIC_/ExerciseProgressView_backup.swift,
 | 
			
		||||
				Models/Exercise.swift,
 | 
			
		||||
				Models/Split.swift,
 | 
			
		||||
				Models/Workout.swift,
 | 
			
		||||
				Models/WorkoutLog.swift,
 | 
			
		||||
				Schema/SchemaV1.swift,
 | 
			
		||||
				Views/Common/CheckboxStatus.swift,
 | 
			
		||||
				Views/WorkoutLog/WorkoutStatus.swift,
 | 
			
		||||
			);
 | 
			
		||||
			target = A45FA1F02E27171A00581607 /* Worksouts Watch App */;
 | 
			
		||||
		};
 | 
			
		||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
 | 
			
		||||
@@ -58,6 +75,7 @@
 | 
			
		||||
			isa = PBXFileSystemSynchronizedRootGroup;
 | 
			
		||||
			exceptions = (
 | 
			
		||||
				A45FA0A12E21B3DE00581607 /* Exceptions for "Workouts" folder in "Workouts" target */,
 | 
			
		||||
				A45FA2C52E2D3CBD00581607 /* Exceptions for "Workouts" folder in "Worksouts Watch App" target */,
 | 
			
		||||
			);
 | 
			
		||||
			path = Workouts;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
@@ -94,6 +112,7 @@
 | 
			
		||||
		A45FA0882E21B3DC00581607 = {
 | 
			
		||||
			isa = PBXGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
				A45FA2C02E2D3C0900581607 /* Shared Models */,
 | 
			
		||||
				A45FA0932E21B3DD00581607 /* Workouts */,
 | 
			
		||||
				A45FA1F22E27171A00581607 /* Worksouts Watch App */,
 | 
			
		||||
				A45FA0922E21B3DD00581607 /* Products */,
 | 
			
		||||
@@ -109,6 +128,13 @@
 | 
			
		||||
			name = Products;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		A45FA2C02E2D3C0900581607 /* Shared Models */ = {
 | 
			
		||||
			isa = PBXGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
			);
 | 
			
		||||
			path = "Shared Models";
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
/* End PBXGroup section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXNativeTarget section */
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,6 @@ struct ContentView: View {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Reports Tab
 | 
			
		||||
            NavigationStack {
 | 
			
		||||
                Text("Reports Placeholder")
 | 
			
		||||
                    .navigationTitle("Reports")
 | 
			
		||||
@@ -36,6 +35,14 @@ struct ContentView: View {
 | 
			
		||||
                Label("Reports", systemImage: "chart.bar")
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            NavigationStack {
 | 
			
		||||
                Text("Achivements")
 | 
			
		||||
                    .navigationTitle("Achievements")
 | 
			
		||||
            }
 | 
			
		||||
            .tabItem {
 | 
			
		||||
                Label("Achivements", systemImage: "star.fill")
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
//            SettingsView()
 | 
			
		||||
//                .tabItem {
 | 
			
		||||
//                    Label("Settings", systemImage: "gear")
 | 
			
		||||
 
 | 
			
		||||
@@ -8,16 +8,19 @@ final class Exercise {
 | 
			
		||||
    var sets: Int = 0
 | 
			
		||||
    var reps: Int = 0
 | 
			
		||||
    var weight: Int = 0
 | 
			
		||||
    var weightLastUpdated: Date = Date()
 | 
			
		||||
    var weightReminderTimeIntervalWeeks: Int = 2
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var split: Split?
 | 
			
		||||
    
 | 
			
		||||
    init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
 | 
			
		||||
    init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int, weightReminderTimeIntervalWeeks: Int = 2) {
 | 
			
		||||
        self.split = split
 | 
			
		||||
        self.name = exerciseName
 | 
			
		||||
        self.order = order
 | 
			
		||||
        self.sets = sets
 | 
			
		||||
        self.reps = reps
 | 
			
		||||
        self.weight = weight
 | 
			
		||||
        self.weightReminderTimeIntervalWeeks = weightReminderTimeIntervalWeeks
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,29 +31,6 @@ final class Split {
 | 
			
		||||
    static let unnamed = "Unnamed Split"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - EditableEntity Conformance
 | 
			
		||||
 | 
			
		||||
extension Split: EditableEntity {
 | 
			
		||||
    var count: Int? {
 | 
			
		||||
        return self.exercises?.count
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func createNew() -> Split {
 | 
			
		||||
        return Split(name: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
        return "Splits"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    static func formView(for model: Split) -> some View {
 | 
			
		||||
        EntityAddEditView(model: model) { $model in
 | 
			
		||||
            SplitFormView(model: $model)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - Identifiable Conformance
 | 
			
		||||
 | 
			
		||||
extension Split: Identifiable {
 | 
			
		||||
@@ -64,58 +41,58 @@ extension Split: Identifiable {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - Private Form View
 | 
			
		||||
 | 
			
		||||
fileprivate struct SplitFormView: View {
 | 
			
		||||
    @Binding var model: Split
 | 
			
		||||
    
 | 
			
		||||
    // Available colors for splits
 | 
			
		||||
    private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
 | 
			
		||||
    
 | 
			
		||||
    // Available system images for splits
 | 
			
		||||
    private let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"]
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Section(header: Text("Name")) {
 | 
			
		||||
            TextField("Name", text: $model.name)
 | 
			
		||||
                .bold()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Appearance")) {
 | 
			
		||||
            Picker("Color", selection: $model.color) {
 | 
			
		||||
                ForEach(availableColors, id: \.self) { colorName in
 | 
			
		||||
                    let tempSplit = Split(name: "", color: colorName)
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Circle()
 | 
			
		||||
                            .fill(tempSplit.getColor())
 | 
			
		||||
                            .frame(width: 20, height: 20)
 | 
			
		||||
                        Text(colorName.capitalized)
 | 
			
		||||
                    }
 | 
			
		||||
                    .tag(colorName)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Picker("Icon", selection: $model.systemImage) {
 | 
			
		||||
                ForEach(availableIcons, id: \.self) { iconName in
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Image(systemName: iconName)
 | 
			
		||||
                            .frame(width: 24, height: 24)
 | 
			
		||||
                        Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized)
 | 
			
		||||
                    }
 | 
			
		||||
                    .tag(iconName)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Exercises")) {
 | 
			
		||||
            NavigationLink {
 | 
			
		||||
                ExerciseListView(split: model)
 | 
			
		||||
            } label: {
 | 
			
		||||
                ListItem(
 | 
			
		||||
                    text: "Exercises",
 | 
			
		||||
                    count: model.exercises?.count ?? 0
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
//// MARK: - Private Form View
 | 
			
		||||
//
 | 
			
		||||
//fileprivate struct SplitFormView: View {
 | 
			
		||||
//    @Binding var model: Split
 | 
			
		||||
//    
 | 
			
		||||
//    // Available colors for splits
 | 
			
		||||
//    private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
 | 
			
		||||
//    
 | 
			
		||||
//    // Available system images for splits
 | 
			
		||||
//    private let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"]
 | 
			
		||||
//    
 | 
			
		||||
//    var body: some View {
 | 
			
		||||
//        Section(header: Text("Name")) {
 | 
			
		||||
//            TextField("Name", text: $model.name)
 | 
			
		||||
//                .bold()
 | 
			
		||||
//        }
 | 
			
		||||
//        
 | 
			
		||||
//        Section(header: Text("Appearance")) {
 | 
			
		||||
//            Picker("Color", selection: $model.color) {
 | 
			
		||||
//                ForEach(availableColors, id: \.self) { colorName in
 | 
			
		||||
//                    let tempSplit = Split(name: "", color: colorName)
 | 
			
		||||
//                    HStack {
 | 
			
		||||
//                        Circle()
 | 
			
		||||
//                            .fill(tempSplit.getColor())
 | 
			
		||||
//                            .frame(width: 20, height: 20)
 | 
			
		||||
//                        Text(colorName.capitalized)
 | 
			
		||||
//                    }
 | 
			
		||||
//                    .tag(colorName)
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//            
 | 
			
		||||
//            Picker("Icon", selection: $model.systemImage) {
 | 
			
		||||
//                ForEach(availableIcons, id: \.self) { iconName in
 | 
			
		||||
//                    HStack {
 | 
			
		||||
//                        Image(systemName: iconName)
 | 
			
		||||
//                            .frame(width: 24, height: 24)
 | 
			
		||||
//                        Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized)
 | 
			
		||||
//                    }
 | 
			
		||||
//                    .tag(iconName)
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//        
 | 
			
		||||
//        Section(header: Text("Exercises")) {
 | 
			
		||||
//            NavigationLink {
 | 
			
		||||
//                ExerciseListView(split: model)
 | 
			
		||||
//            } label: {
 | 
			
		||||
//                ListItem(
 | 
			
		||||
//                    text: "Exercises",
 | 
			
		||||
//                    count: model.exercises?.count ?? 0
 | 
			
		||||
//                )
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								Workouts/Utils/Date+formatDate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Workouts/Utils/Date+formatDate.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formatDate() -> String {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .short
 | 
			
		||||
        return formatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								Workouts/Utils/Date+formatedDate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Workouts/Utils/Date+formatedDate.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formattedDate() -> String {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .short
 | 
			
		||||
        return formatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,30 +9,6 @@
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
enum CheckboxStatus {
 | 
			
		||||
    case checked
 | 
			
		||||
    case unchecked
 | 
			
		||||
    case intermediate
 | 
			
		||||
    case cancelled
 | 
			
		||||
    
 | 
			
		||||
    var color: Color {
 | 
			
		||||
        switch (self) {
 | 
			
		||||
        case .checked: .green
 | 
			
		||||
        case .unchecked: .gray
 | 
			
		||||
        case .intermediate: .yellow
 | 
			
		||||
        case .cancelled: .red
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var systemName: String {
 | 
			
		||||
        switch (self) {
 | 
			
		||||
        case .checked: "checkmark.circle.fill"
 | 
			
		||||
        case .unchecked: "circle"
 | 
			
		||||
        case .intermediate: "ellipsis.circle"
 | 
			
		||||
        case .cancelled: "cross.circle"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct CheckboxListItem: View {
 | 
			
		||||
    var status: CheckboxStatus
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								Workouts/Views/Common/CheckboxStatus.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Workouts/Views/Common/CheckboxStatus.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
//
 | 
			
		||||
// CheckboxStatus.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/20/25 at 11:07 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUICore
 | 
			
		||||
 | 
			
		||||
enum CheckboxStatus {
 | 
			
		||||
    case checked
 | 
			
		||||
    case unchecked
 | 
			
		||||
    case intermediate
 | 
			
		||||
    case cancelled
 | 
			
		||||
    
 | 
			
		||||
    var color: Color {
 | 
			
		||||
        switch (self) {
 | 
			
		||||
        case .checked: .green
 | 
			
		||||
        case .unchecked: .gray
 | 
			
		||||
        case .intermediate: .yellow
 | 
			
		||||
        case .cancelled: .red
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var systemName: String {
 | 
			
		||||
        switch (self) {
 | 
			
		||||
        case .checked: "checkmark.circle.fill"
 | 
			
		||||
        case .unchecked: "circle"
 | 
			
		||||
        case .intermediate: "ellipsis.circle"
 | 
			
		||||
        case .cancelled: "cross.circle"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,19 +16,26 @@ struct ExerciseAddEditView: View {
 | 
			
		||||
 | 
			
		||||
    @State var model: Exercise
 | 
			
		||||
    
 | 
			
		||||
    @State var originalWeight: Int? = nil
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section(header: Text("Exercise")) {
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingExercisePicker = true
 | 
			
		||||
                    }) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text(model.name.isEmpty ? "Select Exercise" : model.name)
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Image(systemName: "chevron.right")
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                    let exerciseName = model.name
 | 
			
		||||
                    if exerciseName.isEmpty {
 | 
			
		||||
                        Button(action: {
 | 
			
		||||
                            showingExercisePicker = true
 | 
			
		||||
                        }) {
 | 
			
		||||
                            HStack {
 | 
			
		||||
                                Text(model.name.isEmpty ? "Select Exercise" : model.name)
 | 
			
		||||
                                Spacer()
 | 
			
		||||
                                Image(systemName: "chevron.right")
 | 
			
		||||
                                    .foregroundColor(.gray)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        ListItem(title: exerciseName)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
@@ -52,6 +59,20 @@ struct ExerciseAddEditView: View {
 | 
			
		||||
                        .frame(width: 130)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section (header: Text("Weight Increase")) {
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Text("Remind every \(model.weightReminderTimeIntervalWeeks) weeks")
 | 
			
		||||
                        Spacer()
 | 
			
		||||
                        Stepper("", value: $model.weightReminderTimeIntervalWeeks, in: 0...366)
 | 
			
		||||
                    }
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Text("Last weight change \(Date().humanTimeInterval(to: model.weightLastUpdated)) ago")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .onAppear {
 | 
			
		||||
                originalWeight = model.weight
 | 
			
		||||
            }
 | 
			
		||||
            .sheet(isPresented: $showingExercisePicker) {
 | 
			
		||||
                ExercisePickerView { exerciseNames in
 | 
			
		||||
@@ -68,6 +89,11 @@ struct ExerciseAddEditView: View {
 | 
			
		||||
                
 | 
			
		||||
                ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                    Button("Save") {
 | 
			
		||||
                        if let originalWeight = originalWeight {
 | 
			
		||||
                            if originalWeight != model.weight {
 | 
			
		||||
                                model.weightLastUpdated = Date()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        try? modelContext.save()
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
import Charts
 | 
			
		||||
 | 
			
		||||
struct ExerciseView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
@@ -98,6 +99,10 @@ struct ExerciseView: View {
 | 
			
		||||
                }
 | 
			
		||||
                    .font(.title)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Section(header: Text("Progress Tracking")) {
 | 
			
		||||
                WeightProgressionChartView(exerciseName: workoutLog.exerciseName)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle("\(workoutLog.exerciseName)")
 | 
			
		||||
        .navigationDestination(item: $navigateTo) { nextLog in
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								Workouts/Views/Exercises/WeightProgressionChartView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								Workouts/Views/Exercises/WeightProgressionChartView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
//
 | 
			
		||||
// WeightProgressionChartView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created on 7/20/25.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import Charts
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct WeightProgressionChartView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    
 | 
			
		||||
    let exerciseName: String
 | 
			
		||||
    @State private var weightData: [WeightDataPoint] = []
 | 
			
		||||
    @State private var isLoading: Bool = true
 | 
			
		||||
    @State private var motivationalMessage: String = ""
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(alignment: .leading) {
 | 
			
		||||
            if isLoading {
 | 
			
		||||
                ProgressView("Loading data...")
 | 
			
		||||
            } else if weightData.isEmpty {
 | 
			
		||||
                Text("No weight history available yet.")
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
                    .frame(maxWidth: .infinity, alignment: .center)
 | 
			
		||||
                    .padding()
 | 
			
		||||
            } else {
 | 
			
		||||
                Text("Weight Progression")
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
                    .padding(.bottom, 4)
 | 
			
		||||
                
 | 
			
		||||
                Chart {
 | 
			
		||||
                    ForEach(weightData) { dataPoint in
 | 
			
		||||
                        LineMark(
 | 
			
		||||
                            x: .value("Date", dataPoint.date),
 | 
			
		||||
                            y: .value("Weight", dataPoint.weight)
 | 
			
		||||
                        )
 | 
			
		||||
                        .foregroundStyle(Color.blue.gradient)
 | 
			
		||||
                        .interpolationMethod(.catmullRom)
 | 
			
		||||
                        
 | 
			
		||||
                        PointMark(
 | 
			
		||||
                            x: .value("Date", dataPoint.date),
 | 
			
		||||
                            y: .value("Weight", dataPoint.weight)
 | 
			
		||||
                        )
 | 
			
		||||
                        .foregroundStyle(Color.blue)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .chartYScale(domain: .automatic(includesZero: false))
 | 
			
		||||
                .chartXAxis {
 | 
			
		||||
                    AxisMarks(values: .automatic) { value in
 | 
			
		||||
                        AxisGridLine()
 | 
			
		||||
                        AxisValueLabel(format: .dateTime.month().day())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .frame(height: 200)
 | 
			
		||||
                .padding(.bottom, 8)
 | 
			
		||||
                
 | 
			
		||||
                if !motivationalMessage.isEmpty {
 | 
			
		||||
                    Text(motivationalMessage)
 | 
			
		||||
                        .font(.subheadline)
 | 
			
		||||
                        .foregroundColor(.primary)
 | 
			
		||||
                        .padding()
 | 
			
		||||
                        .background(Color.blue.opacity(0.1))
 | 
			
		||||
                        .cornerRadius(8)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            loadWeightData()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func loadWeightData() {
 | 
			
		||||
        isLoading = true
 | 
			
		||||
        
 | 
			
		||||
        // Create a fetch descriptor to get workout logs for this exercise
 | 
			
		||||
        let descriptor = FetchDescriptor<WorkoutLog>(
 | 
			
		||||
            predicate: #Predicate<WorkoutLog> { log in
 | 
			
		||||
                log.exerciseName == exerciseName && log.completed == true
 | 
			
		||||
            },
 | 
			
		||||
            sortBy: [SortDescriptor(\WorkoutLog.date)]
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        // Fetch the data
 | 
			
		||||
        if let logs = try? modelContext.fetch(descriptor) {
 | 
			
		||||
            // Convert to data points
 | 
			
		||||
            weightData = logs.map { log in
 | 
			
		||||
                WeightDataPoint(date: log.date, weight: log.weight)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Generate motivational message based on progress
 | 
			
		||||
            generateMotivationalMessage()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        isLoading = false
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func generateMotivationalMessage() {
 | 
			
		||||
        guard weightData.count >= 2 else {
 | 
			
		||||
            motivationalMessage = "Complete more workouts to track your progress!"
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Calculate progress metrics
 | 
			
		||||
        let firstWeight = weightData.first?.weight ?? 0
 | 
			
		||||
        let currentWeight = weightData.last?.weight ?? 0
 | 
			
		||||
        let weightDifference = currentWeight - firstWeight
 | 
			
		||||
        
 | 
			
		||||
        // Generate appropriate message based on progress
 | 
			
		||||
        if weightDifference > 0 {
 | 
			
		||||
            let percentIncrease = Int((Double(weightDifference) / Double(firstWeight)) * 100)
 | 
			
		||||
            if percentIncrease >= 20 {
 | 
			
		||||
                motivationalMessage = "Amazing progress! You've increased your weight by \(weightDifference) lbs (\(percentIncrease)%)! 💪"
 | 
			
		||||
            } else if percentIncrease >= 10 {
 | 
			
		||||
                motivationalMessage = "Great job! You've increased your weight by \(weightDifference) lbs (\(percentIncrease)%)! 🎉"
 | 
			
		||||
            } else {
 | 
			
		||||
                motivationalMessage = "You're making progress! Weight increased by \(weightDifference) lbs. Keep it up! 👍"
 | 
			
		||||
            }
 | 
			
		||||
        } else if weightDifference == 0 {
 | 
			
		||||
            motivationalMessage = "You're maintaining consistent weight. Focus on form and consider increasing when ready!"
 | 
			
		||||
        } else {
 | 
			
		||||
            motivationalMessage = "Your current weight is lower than when you started. Adjust your training as needed and keep pushing!"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Data structure for chart points
 | 
			
		||||
struct WeightDataPoint: Identifiable {
 | 
			
		||||
    let id = UUID()
 | 
			
		||||
    let date: Date
 | 
			
		||||
    let weight: Int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    WeightProgressionChartView(exerciseName: "Bench Press")
 | 
			
		||||
        .modelContainer(for: [WorkoutLog.self], inMemory: true)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								Workouts/Views/Settings/SettingsView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Workouts/Views/Settings/SettingsView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
//
 | 
			
		||||
// SettingsView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/20/25 at 8:14 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct SettingsView: View {
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section (header: Text("Options")) {
 | 
			
		||||
                    
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,7 +20,7 @@ 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
 | 
			
		||||
@@ -86,61 +86,61 @@ struct WorkoutListView: View {
 | 
			
		||||
            } message: {
 | 
			
		||||
                Text("Are you sure you want to delete this workout?")
 | 
			
		||||
            }
 | 
			
		||||
            .sheet(isPresented: $showingSplitPicker) {
 | 
			
		||||
                SplitPickerView { split in
 | 
			
		||||
                    let workout = Workout(start: Date(), end: Date(), split: split)
 | 
			
		||||
                    modelContext.insert(workout)
 | 
			
		||||
                    if let exercises = split.exercises {
 | 
			
		||||
                        for exercise in exercises {
 | 
			
		||||
                            let workoutLog = WorkoutLog(
 | 
			
		||||
                                workout: workout,
 | 
			
		||||
                                exerciseName: exercise.name,
 | 
			
		||||
                                date: Date(),
 | 
			
		||||
                                order: exercise.order,
 | 
			
		||||
                                sets: exercise.sets,
 | 
			
		||||
                                reps: exercise.reps,
 | 
			
		||||
                                weight: exercise.weight
 | 
			
		||||
                            )
 | 
			
		||||
                            modelContext.insert(workoutLog)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    try? modelContext.save()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
//            .sheet(isPresented: $showingSplitPicker) {
 | 
			
		||||
//                SplitPickerView { split in
 | 
			
		||||
//                    let workout = Workout(start: Date(), end: Date(), split: split)
 | 
			
		||||
//                    modelContext.insert(workout)
 | 
			
		||||
//                    if let exercises = split.exercises {
 | 
			
		||||
//                        for exercise in exercises {
 | 
			
		||||
//                            let workoutLog = WorkoutLog(
 | 
			
		||||
//                                workout: workout,
 | 
			
		||||
//                                exerciseName: exercise.name,
 | 
			
		||||
//                                date: Date(),
 | 
			
		||||
//                                order: exercise.order,
 | 
			
		||||
//                                sets: exercise.sets,
 | 
			
		||||
//                                reps: exercise.reps,
 | 
			
		||||
//                                weight: exercise.weight
 | 
			
		||||
//                            )
 | 
			
		||||
//                            modelContext.insert(workoutLog)
 | 
			
		||||
//                        }
 | 
			
		||||
//                    }
 | 
			
		||||
//                    try? modelContext.save()
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formattedDate() -> String {
 | 
			
		||||
        let calendar = Calendar.current
 | 
			
		||||
        let now = Date()
 | 
			
		||||
 | 
			
		||||
        let timeFormatter = DateFormatter()
 | 
			
		||||
        timeFormatter.dateFormat = "h:mm a"
 | 
			
		||||
 | 
			
		||||
        let dateFormatter = DateFormatter()
 | 
			
		||||
 | 
			
		||||
        let date = self
 | 
			
		||||
        
 | 
			
		||||
        if calendar.isDateInToday(date) {
 | 
			
		||||
            return "Today @ \(timeFormatter.string(from: date))"
 | 
			
		||||
        } else if calendar.isDateInYesterday(date) {
 | 
			
		||||
            return "Yesterday @ \(timeFormatter.string(from: date))"
 | 
			
		||||
        } else {
 | 
			
		||||
            let dateComponents = calendar.dateComponents([.year], from: date)
 | 
			
		||||
            let currentYearComponents = calendar.dateComponents([.year], from: now)
 | 
			
		||||
 | 
			
		||||
            if dateComponents.year == currentYearComponents.year {
 | 
			
		||||
                dateFormatter.dateFormat = "M/d"
 | 
			
		||||
            } else {
 | 
			
		||||
                dateFormatter.dateFormat = "M/d/yyyy"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let dateString = dateFormatter.string(from: date)
 | 
			
		||||
            let timeString = timeFormatter.string(from: date)
 | 
			
		||||
            return "\(dateString) @ \(timeString)"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
//extension Date {
 | 
			
		||||
//    func formattedDate() -> String {
 | 
			
		||||
//        let calendar = Calendar.current
 | 
			
		||||
//        let now = Date()
 | 
			
		||||
//
 | 
			
		||||
//        let timeFormatter = DateFormatter()
 | 
			
		||||
//        timeFormatter.dateFormat = "h:mm a"
 | 
			
		||||
//
 | 
			
		||||
//        let dateFormatter = DateFormatter()
 | 
			
		||||
//
 | 
			
		||||
//        let date = self
 | 
			
		||||
//        
 | 
			
		||||
//        if calendar.isDateInToday(date) {
 | 
			
		||||
//            return "Today @ \(timeFormatter.string(from: date))"
 | 
			
		||||
//        } else if calendar.isDateInYesterday(date) {
 | 
			
		||||
//            return "Yesterday @ \(timeFormatter.string(from: date))"
 | 
			
		||||
//        } else {
 | 
			
		||||
//            let dateComponents = calendar.dateComponents([.year], from: date)
 | 
			
		||||
//            let currentYearComponents = calendar.dateComponents([.year], from: now)
 | 
			
		||||
//
 | 
			
		||||
//            if dateComponents.year == currentYearComponents.year {
 | 
			
		||||
//                dateFormatter.dateFormat = "M/d"
 | 
			
		||||
//            } else {
 | 
			
		||||
//                dateFormatter.dateFormat = "M/d/yyyy"
 | 
			
		||||
//            }
 | 
			
		||||
//
 | 
			
		||||
//            let dateString = dateFormatter.string(from: date)
 | 
			
		||||
//            let timeString = timeFormatter.string(from: date)
 | 
			
		||||
//            return "\(dateString) @ \(timeString)"
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//
 | 
			
		||||
//}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								Workouts/_ATTIC_/ContentView_backup.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Workouts/_ATTIC_/ContentView_backup.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
//
 | 
			
		||||
// ContentView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/15/25 at 7:09 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
//import SwiftUI
 | 
			
		||||
//import SwiftData
 | 
			
		||||
//
 | 
			
		||||
//struct ContentView: View {
 | 
			
		||||
//    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
//    
 | 
			
		||||
//    let completedStatus = WorkoutStatus.completed
 | 
			
		||||
//    
 | 
			
		||||
//    @Query(filter: #Predicate<Workout> { workout in
 | 
			
		||||
//        workout.status?.rawValue != WorkoutStatus.completed.rawValue
 | 
			
		||||
//    }, sort: \Workout.start, order: .reverse) var activeWorkouts: [Workout]
 | 
			
		||||
//    
 | 
			
		||||
//    var body: some View {
 | 
			
		||||
//        NavigationStack {
 | 
			
		||||
//            if activeWorkouts.isEmpty {
 | 
			
		||||
//                NoActiveWorkoutView()
 | 
			
		||||
//            } else if let currentWorkout = activeWorkouts.first {
 | 
			
		||||
//                WorkoutLogListView(workout: currentWorkout)
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
//struct NoActiveWorkoutView: View {
 | 
			
		||||
//    var body: some View {
 | 
			
		||||
//        VStack(spacing: 16) {
 | 
			
		||||
//            Image(systemName: "dumbbell.fill")
 | 
			
		||||
//                .font(.system(size: 40))
 | 
			
		||||
//                .foregroundStyle(.gray)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("No Active Workout")
 | 
			
		||||
//                .font(.headline)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("Start a workout in the main app")
 | 
			
		||||
//                .font(.caption)
 | 
			
		||||
//                .foregroundStyle(.gray)
 | 
			
		||||
//                .multilineTextAlignment(.center)
 | 
			
		||||
//        }
 | 
			
		||||
//        .padding()
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
////#Preview {
 | 
			
		||||
////    ContentView()
 | 
			
		||||
////        .modelContainer(AppContainer.preview)
 | 
			
		||||
////}
 | 
			
		||||
							
								
								
									
										317
									
								
								Workouts/_ATTIC_/ExerciseProgressView_backup.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								Workouts/_ATTIC_/ExerciseProgressView_backup.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,317 @@
 | 
			
		||||
//import SwiftUI
 | 
			
		||||
//import SwiftData
 | 
			
		||||
//import WatchKit
 | 
			
		||||
//
 | 
			
		||||
//// Enum to track the current phase of the exercise
 | 
			
		||||
//enum ExercisePhase {
 | 
			
		||||
//    case notStarted
 | 
			
		||||
//    case exercising(setNumber: Int)
 | 
			
		||||
//    case resting(setNumber: Int, elapsedSeconds: Int)
 | 
			
		||||
//    case completed
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
//struct ExerciseProgressView: View {
 | 
			
		||||
//    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
//    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
//    
 | 
			
		||||
//    let log: WorkoutLog
 | 
			
		||||
//    
 | 
			
		||||
//    @State private var phase: ExercisePhase = .notStarted
 | 
			
		||||
//    @State private var currentSetNumber: Int = 0
 | 
			
		||||
//    @State private var restingSeconds: Int = 0
 | 
			
		||||
//    @State private var timer: Timer?
 | 
			
		||||
//    @State private var hapticTimer: Timer?
 | 
			
		||||
//    @State private var hapticSeconds: Int = 0
 | 
			
		||||
//    
 | 
			
		||||
//    var body: some View {
 | 
			
		||||
//        ScrollView {
 | 
			
		||||
//            VStack(spacing: 16) {
 | 
			
		||||
//                Text(log.exerciseName)
 | 
			
		||||
//                    .font(.headline)
 | 
			
		||||
//                    .multilineTextAlignment(.center)
 | 
			
		||||
//                
 | 
			
		||||
//                switch phase {
 | 
			
		||||
//                case .notStarted:
 | 
			
		||||
//                    startView
 | 
			
		||||
//                case .exercising(let setNumber):
 | 
			
		||||
//                    exercisingView(setNumber: setNumber)
 | 
			
		||||
//                case .resting(let setNumber, let elapsedSeconds):
 | 
			
		||||
//                    restingView(setNumber: setNumber, elapsedSeconds: elapsedSeconds)
 | 
			
		||||
//                case .completed:
 | 
			
		||||
//                    completedView
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//            .padding()
 | 
			
		||||
//        }
 | 
			
		||||
//        .navigationTitle("Progress")
 | 
			
		||||
//        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
//        .onDisappear {
 | 
			
		||||
//            stopTimers()
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private var startView: some View {
 | 
			
		||||
//        VStack(spacing: 16) {
 | 
			
		||||
//            Text("Ready to start")
 | 
			
		||||
//                .font(.title3)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs")
 | 
			
		||||
//                .font(.subheadline)
 | 
			
		||||
//                .foregroundStyle(.secondary)
 | 
			
		||||
//            
 | 
			
		||||
//            Button(action: startExercise) {
 | 
			
		||||
//                Text("Start First Set")
 | 
			
		||||
//                    .font(.headline)
 | 
			
		||||
//                    .foregroundStyle(.white)
 | 
			
		||||
//                    .frame(maxWidth: .infinity)
 | 
			
		||||
//                    .padding(.vertical, 8)
 | 
			
		||||
//                    .background(Color.blue)
 | 
			
		||||
//                    .cornerRadius(8)
 | 
			
		||||
//            }
 | 
			
		||||
//            .buttonStyle(PlainButtonStyle())
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func exercisingView(setNumber: Int) -> some View {
 | 
			
		||||
//        VStack(spacing: 16) {
 | 
			
		||||
//            Text("Set \(setNumber) of \(log.sets)")
 | 
			
		||||
//                .font(.title3)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("\(log.reps) reps × \(log.weight) lbs")
 | 
			
		||||
//                .font(.subheadline)
 | 
			
		||||
//                .foregroundStyle(.secondary)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("In progress: \(hapticSeconds)s")
 | 
			
		||||
//                .font(.body)
 | 
			
		||||
//                .monospacedDigit()
 | 
			
		||||
//            
 | 
			
		||||
//            HStack {
 | 
			
		||||
//                Button(action: completeSet) {
 | 
			
		||||
//                    Text("Complete")
 | 
			
		||||
//                        .font(.headline)
 | 
			
		||||
//                        .foregroundStyle(.white)
 | 
			
		||||
//                        .frame(maxWidth: .infinity)
 | 
			
		||||
//                        .padding(.vertical, 8)
 | 
			
		||||
//                        .background(Color.green)
 | 
			
		||||
//                        .cornerRadius(8)
 | 
			
		||||
//                }
 | 
			
		||||
//                .buttonStyle(PlainButtonStyle())
 | 
			
		||||
//                
 | 
			
		||||
//                Button(action: cancelSet) {
 | 
			
		||||
//                    Text("Cancel")
 | 
			
		||||
//                        .font(.headline)
 | 
			
		||||
//                        .foregroundStyle(.white)
 | 
			
		||||
//                        .frame(maxWidth: .infinity)
 | 
			
		||||
//                        .padding(.vertical, 8)
 | 
			
		||||
//                        .background(Color.red)
 | 
			
		||||
//                        .cornerRadius(8)
 | 
			
		||||
//                }
 | 
			
		||||
//                .buttonStyle(PlainButtonStyle())
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//        .gesture(
 | 
			
		||||
//            DragGesture(minimumDistance: 20)
 | 
			
		||||
//                .onEnded { gesture in
 | 
			
		||||
//                    if gesture.translation.width < 0 {
 | 
			
		||||
//                        // Swipe left to complete
 | 
			
		||||
//                        completeSet()
 | 
			
		||||
//                    } else if gesture.translation.width > 0 {
 | 
			
		||||
//                        // Swipe right to cancel
 | 
			
		||||
//                        cancelSet()
 | 
			
		||||
//                    }
 | 
			
		||||
//                }
 | 
			
		||||
//        )
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func restingView(setNumber: Int, elapsedSeconds: Int) -> some View {
 | 
			
		||||
//        VStack(spacing: 16) {
 | 
			
		||||
//            Text("Rest")
 | 
			
		||||
//                .font(.title3)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("After Set \(setNumber) of \(log.sets)")
 | 
			
		||||
//                .font(.subheadline)
 | 
			
		||||
//                .foregroundStyle(.secondary)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("Resting: \(elapsedSeconds)s")
 | 
			
		||||
//                .font(.body)
 | 
			
		||||
//                .monospacedDigit()
 | 
			
		||||
//            
 | 
			
		||||
//            Button(action: {
 | 
			
		||||
//                if setNumber < log.sets {
 | 
			
		||||
//                    startNextSet()
 | 
			
		||||
//                } else {
 | 
			
		||||
//                    completeExercise()
 | 
			
		||||
//                }
 | 
			
		||||
//            }) {
 | 
			
		||||
//                Text(setNumber < log.sets ? "Start Next Set" : "Complete Exercise")
 | 
			
		||||
//                    .font(.headline)
 | 
			
		||||
//                    .foregroundStyle(.white)
 | 
			
		||||
//                    .frame(maxWidth: .infinity)
 | 
			
		||||
//                    .padding(.vertical, 8)
 | 
			
		||||
//                    .background(Color.blue)
 | 
			
		||||
//                    .cornerRadius(8)
 | 
			
		||||
//            }
 | 
			
		||||
//            .buttonStyle(PlainButtonStyle())
 | 
			
		||||
//        }
 | 
			
		||||
//        .gesture(
 | 
			
		||||
//            DragGesture(minimumDistance: 20)
 | 
			
		||||
//                .onEnded { gesture in
 | 
			
		||||
//                    if gesture.translation.width < 0 {
 | 
			
		||||
//                        // Swipe left to start next set or complete
 | 
			
		||||
//                        if setNumber < log.sets {
 | 
			
		||||
//                            startNextSet()
 | 
			
		||||
//                        } else {
 | 
			
		||||
//                            completeExercise()
 | 
			
		||||
//                        }
 | 
			
		||||
//                    }
 | 
			
		||||
//                }
 | 
			
		||||
//        )
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private var completedView: some View {
 | 
			
		||||
//        VStack(spacing: 16) {
 | 
			
		||||
//            Image(systemName: "checkmark.circle.fill")
 | 
			
		||||
//                .font(.system(size: 50))
 | 
			
		||||
//                .foregroundStyle(.green)
 | 
			
		||||
//            
 | 
			
		||||
//            Text("Exercise Completed!")
 | 
			
		||||
//                .font(.title3)
 | 
			
		||||
//            
 | 
			
		||||
//            Button(action: {
 | 
			
		||||
//                dismiss()
 | 
			
		||||
//            }) {
 | 
			
		||||
//                Text("Return to Workout")
 | 
			
		||||
//                    .font(.headline)
 | 
			
		||||
//                    .foregroundStyle(.white)
 | 
			
		||||
//                    .frame(maxWidth: .infinity)
 | 
			
		||||
//                    .padding(.vertical, 8)
 | 
			
		||||
//                    .background(Color.blue)
 | 
			
		||||
//                    .cornerRadius(8)
 | 
			
		||||
//            }
 | 
			
		||||
//            .buttonStyle(PlainButtonStyle())
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    // MARK: - Actions
 | 
			
		||||
//    
 | 
			
		||||
//    private func startExercise() {
 | 
			
		||||
//        currentSetNumber = 1
 | 
			
		||||
//        phase = .exercising(setNumber: currentSetNumber)
 | 
			
		||||
//        
 | 
			
		||||
//        // Update workout log status
 | 
			
		||||
//        log.status = .inProgress
 | 
			
		||||
//        try? modelContext.save()
 | 
			
		||||
//        
 | 
			
		||||
//        // Start haptic timer
 | 
			
		||||
//        startHapticTimer()
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func completeSet() {
 | 
			
		||||
//        stopHapticTimer()
 | 
			
		||||
//        
 | 
			
		||||
//        // Start rest phase
 | 
			
		||||
//        restingSeconds = 0
 | 
			
		||||
//        phase = .resting(setNumber: currentSetNumber, elapsedSeconds: restingSeconds)
 | 
			
		||||
//        
 | 
			
		||||
//        // Start rest timer
 | 
			
		||||
//        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
 | 
			
		||||
//            restingSeconds += 1
 | 
			
		||||
//            phase = .resting(setNumber: currentSetNumber, elapsedSeconds: restingSeconds)
 | 
			
		||||
//        }
 | 
			
		||||
//        
 | 
			
		||||
//        // Start haptic timer for rest phase
 | 
			
		||||
//        startHapticTimer()
 | 
			
		||||
//        
 | 
			
		||||
//        // Play completion haptic
 | 
			
		||||
//        WKInterfaceDevice.current().play(.success)
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func cancelSet() {
 | 
			
		||||
//        // Just go back to the previous state
 | 
			
		||||
//        if currentSetNumber > 1 {
 | 
			
		||||
//            currentSetNumber -= 1
 | 
			
		||||
//            phase = .resting(setNumber: currentSetNumber, elapsedSeconds: 0)
 | 
			
		||||
//        } else {
 | 
			
		||||
//            phase = .notStarted
 | 
			
		||||
//        }
 | 
			
		||||
//        
 | 
			
		||||
//        stopHapticTimer()
 | 
			
		||||
//        stopTimers()
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func startNextSet() {
 | 
			
		||||
//        stopTimers()
 | 
			
		||||
//        
 | 
			
		||||
//        currentSetNumber += 1
 | 
			
		||||
//        phase = .exercising(setNumber: currentSetNumber)
 | 
			
		||||
//        
 | 
			
		||||
//        // Start haptic timer for next set
 | 
			
		||||
//        startHapticTimer()
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func completeExercise() {
 | 
			
		||||
//        stopTimers()
 | 
			
		||||
//        
 | 
			
		||||
//        // Update workout log
 | 
			
		||||
//        log.completed = true
 | 
			
		||||
//        log.status = .completed
 | 
			
		||||
//        try? modelContext.save()
 | 
			
		||||
//        
 | 
			
		||||
//        // Show completion screen
 | 
			
		||||
//        phase = .completed
 | 
			
		||||
//        
 | 
			
		||||
//        // Play completion haptic
 | 
			
		||||
//        WKInterfaceDevice.current().play(.success)
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    // MARK: - Timer Management
 | 
			
		||||
//    
 | 
			
		||||
//    private func startHapticTimer() {
 | 
			
		||||
//        hapticSeconds = 0
 | 
			
		||||
//        hapticTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
 | 
			
		||||
//            hapticSeconds += 1
 | 
			
		||||
//            
 | 
			
		||||
//            // Provide haptic feedback based on time intervals
 | 
			
		||||
//            if hapticSeconds % 60 == 0 {
 | 
			
		||||
//                // Triple tap every 60 seconds
 | 
			
		||||
//                WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
//                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
 | 
			
		||||
//                    WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
//                }
 | 
			
		||||
//                DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
 | 
			
		||||
//                    WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
//                }
 | 
			
		||||
//            } else if hapticSeconds % 30 == 0 {
 | 
			
		||||
//                // Double tap every 30 seconds
 | 
			
		||||
//                WKInterfaceDevice.current().play(.click)
 | 
			
		||||
//                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
 | 
			
		||||
//                    WKInterfaceDevice.current().play(.click)
 | 
			
		||||
//                }
 | 
			
		||||
//            } else if hapticSeconds % 10 == 0 {
 | 
			
		||||
//                // Light tap every 10 seconds
 | 
			
		||||
//                WKInterfaceDevice.current().play(.click)
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func stopHapticTimer() {
 | 
			
		||||
//        hapticTimer?.invalidate()
 | 
			
		||||
//        hapticTimer = nil
 | 
			
		||||
//        hapticSeconds = 0
 | 
			
		||||
//    }
 | 
			
		||||
//    
 | 
			
		||||
//    private func stopTimers() {
 | 
			
		||||
//        timer?.invalidate()
 | 
			
		||||
//        timer = nil
 | 
			
		||||
//        stopHapticTimer()
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    let container = AppContainer.preview
 | 
			
		||||
//    let workout = Workout(start: Date(), end: Date(), split: nil)
 | 
			
		||||
//    let log = WorkoutLog(workout: workout, exerciseName: "Bench Press", date: Date(), sets: 3, reps: 10, weight: 135)
 | 
			
		||||
//    
 | 
			
		||||
//    return ExerciseProgressView(log: log)
 | 
			
		||||
//        .modelContainer(container)
 | 
			
		||||
//}
 | 
			
		||||
@@ -2,26 +2,89 @@
 | 
			
		||||
// ContentView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/15/25 at 7:09 PM.
 | 
			
		||||
// Created by rzen on 7/15/25 at 7:09 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct ContentView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    
 | 
			
		||||
    // Use string literal for completed status to avoid enum reference in predicate
 | 
			
		||||
//    @Query(filter: #Predicate<Workout> { workout in
 | 
			
		||||
//        workout.status?.rawValue != 3
 | 
			
		||||
//    }, sort: \Workout.start, order: .reverse) var activeWorkouts: [Workout]
 | 
			
		||||
    
 | 
			
		||||
//    @Query(sort: [SortDescriptor(\Workout.start)]) var allWorkouts: [Workout]
 | 
			
		||||
    
 | 
			
		||||
    @State var activeWorkouts: [Workout] = []
 | 
			
		||||
    @State var splits: [Split] = []
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            Image(systemName: "globe")
 | 
			
		||||
                .imageScale(.large)
 | 
			
		||||
                .foregroundStyle(.tint)
 | 
			
		||||
            Text("Hello, world!")
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            if activeWorkouts.isEmpty {
 | 
			
		||||
                NoActiveWorkoutView()
 | 
			
		||||
            } else {
 | 
			
		||||
                ActiveWorkoutListView(workouts: activeWorkouts)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            loadSplits()
 | 
			
		||||
            loadActiveWorkouts()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func loadActiveWorkouts () {
 | 
			
		||||
        do {
 | 
			
		||||
            print("loading active workouts")
 | 
			
		||||
            self.activeWorkouts = try modelContext.fetch(FetchDescriptor<Workout>(
 | 
			
		||||
                sortBy: [
 | 
			
		||||
                    SortDescriptor(\Workout.start)
 | 
			
		||||
                ]
 | 
			
		||||
            ))
 | 
			
		||||
            print("loaded active workouts \(activeWorkouts.count)")
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("ERROR: failed to load active workouts \(error)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct NoActiveWorkoutView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(spacing: 16) {
 | 
			
		||||
            Image(systemName: "dumbbell.fill")
 | 
			
		||||
                .font(.system(size: 40))
 | 
			
		||||
                .foregroundStyle(.gray)
 | 
			
		||||
            
 | 
			
		||||
            Text("No Active Workout")
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
            
 | 
			
		||||
            Text("Start a workout in the main app")
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
                .foregroundStyle(.gray)
 | 
			
		||||
                .multilineTextAlignment(.center)
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    ContentView()
 | 
			
		||||
}
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    ContentView()
 | 
			
		||||
//        .modelContainer(AppContainer.preview)
 | 
			
		||||
//}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										202
									
								
								Worksouts Watch App/Schema/AppContainer.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								Worksouts Watch App/Schema/AppContainer.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,202 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
final class AppContainer {
 | 
			
		||||
    static let logger = AppLogger(
 | 
			
		||||
        subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts.watchkitapp",
 | 
			
		||||
        category: "AppContainer"
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    static func create() -> ModelContainer {
 | 
			
		||||
        // Using the current models directly without migration plan to avoid reference errors
 | 
			
		||||
        let schema = Schema(SchemaVersion.models)
 | 
			
		||||
        
 | 
			
		||||
        #if targetEnvironment(simulator) && os(watchOS)
 | 
			
		||||
            // Use local-only storage for watchOS simulator
 | 
			
		||||
            let configuration = ModelConfiguration(isStoredInMemoryOnly: false)
 | 
			
		||||
            logger.info("Creating local-only database for watchOS simulator")
 | 
			
		||||
            
 | 
			
		||||
            do {
 | 
			
		||||
                let container = try ModelContainer(for: schema, configurations: configuration)
 | 
			
		||||
                
 | 
			
		||||
                // Populate with test data if needed
 | 
			
		||||
                Task { @MainActor in
 | 
			
		||||
                    await populateSimulatorData(container: container)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                return container
 | 
			
		||||
            } catch {
 | 
			
		||||
                logger.error("Failed to create simulator ModelContainer: \(error.localizedDescription)")
 | 
			
		||||
                fatalError("Failed to create simulator ModelContainer: \(error.localizedDescription)")
 | 
			
		||||
            }
 | 
			
		||||
        #else
 | 
			
		||||
            // Use CloudKit for real devices
 | 
			
		||||
            let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
 | 
			
		||||
            logger.info("Creating CloudKit database for real device")
 | 
			
		||||
            
 | 
			
		||||
            let container = try! ModelContainer(for: schema, configurations: configuration)
 | 
			
		||||
            return container
 | 
			
		||||
        #endif
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @MainActor
 | 
			
		||||
    static var preview: ModelContainer {
 | 
			
		||||
        let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            let schema = Schema(SchemaVersion.models)
 | 
			
		||||
            let container = try ModelContainer(for: schema, configurations: configuration)
 | 
			
		||||
            return container
 | 
			
		||||
        } catch {
 | 
			
		||||
            fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @MainActor
 | 
			
		||||
    private static func populateSimulatorData(container: ModelContainer) async {
 | 
			
		||||
        let context = container.mainContext
 | 
			
		||||
        
 | 
			
		||||
        // Check if data already exists
 | 
			
		||||
        let fetchDescriptor = FetchDescriptor<Split>()
 | 
			
		||||
        guard (try? context.fetch(fetchDescriptor))?.isEmpty ?? true else {
 | 
			
		||||
            logger.info("Simulator database already has data, skipping population")
 | 
			
		||||
            return // Data already exists
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        logger.info("Populating simulator database with test data from pf-starter-exercises.yaml")
 | 
			
		||||
        
 | 
			
		||||
        // Create splits
 | 
			
		||||
        let upperBodySplit = Split(name: "Upper Body", color: "blue", systemImage: "figure.strengthtraining.traditional", order: 0)
 | 
			
		||||
        let lowerBodySplit = Split(name: "Lower Body", color: "green", systemImage: "figure.run", order: 1)
 | 
			
		||||
        let fullBodySplit = Split(name: "Full Body", color: "purple", systemImage: "figure.mixed.cardio", order: 2)
 | 
			
		||||
        let coreSplit = Split(name: "Core", color: "red", systemImage: "figure.core.training", order: 3)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(upperBodySplit)
 | 
			
		||||
        context.insert(lowerBodySplit)
 | 
			
		||||
        context.insert(fullBodySplit)
 | 
			
		||||
        context.insert(coreSplit)
 | 
			
		||||
        
 | 
			
		||||
        // Create exercises based on pf-starter-exercises.yaml
 | 
			
		||||
        
 | 
			
		||||
        // Upper Body Exercises
 | 
			
		||||
        let latPullDown = Exercise(split: upperBodySplit, exerciseName: "Lat Pull Down", order: 0, sets: 3, reps: 12, weight: 120)
 | 
			
		||||
        let seatedRow = Exercise(split: upperBodySplit, exerciseName: "Seated Row", order: 1, sets: 3, reps: 12, weight: 110)
 | 
			
		||||
        let shoulderPress = Exercise(split: upperBodySplit, exerciseName: "Shoulder Press", order: 2, sets: 3, reps: 10, weight: 90)
 | 
			
		||||
        let chestPress = Exercise(split: upperBodySplit, exerciseName: "Chest Press", order: 3, sets: 3, reps: 10, weight: 130)
 | 
			
		||||
        let tricepPress = Exercise(split: upperBodySplit, exerciseName: "Tricep Press", order: 4, sets: 3, reps: 12, weight: 70)
 | 
			
		||||
        let armCurl = Exercise(split: upperBodySplit, exerciseName: "Arm Curl", order: 5, sets: 3, reps: 12, weight: 60)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(latPullDown)
 | 
			
		||||
        context.insert(seatedRow)
 | 
			
		||||
        context.insert(shoulderPress)
 | 
			
		||||
        context.insert(chestPress)
 | 
			
		||||
        context.insert(tricepPress)
 | 
			
		||||
        context.insert(armCurl)
 | 
			
		||||
        
 | 
			
		||||
        // Core Exercises
 | 
			
		||||
        let abdominal = Exercise(split: coreSplit, exerciseName: "Abdominal", order: 0, sets: 3, reps: 15, weight: 80)
 | 
			
		||||
        let rotary = Exercise(split: coreSplit, exerciseName: "Rotary", order: 1, sets: 3, reps: 15, weight: 70)
 | 
			
		||||
        let plank = Exercise(split: coreSplit, exerciseName: "Plank", order: 2, sets: 3, reps: 1, weight: 0) // Reps as time in minutes
 | 
			
		||||
        let russianTwists = Exercise(split: coreSplit, exerciseName: "Russian Twists", order: 3, sets: 3, reps: 20, weight: 25)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(abdominal)
 | 
			
		||||
        context.insert(rotary)
 | 
			
		||||
        context.insert(plank)
 | 
			
		||||
        context.insert(russianTwists)
 | 
			
		||||
        
 | 
			
		||||
        // Lower Body Exercises
 | 
			
		||||
        let legPress = Exercise(split: lowerBodySplit, exerciseName: "Leg Press", order: 0, sets: 3, reps: 12, weight: 200)
 | 
			
		||||
        let legExtension = Exercise(split: lowerBodySplit, exerciseName: "Leg Extension", order: 1, sets: 3, reps: 12, weight: 110)
 | 
			
		||||
        let legCurl = Exercise(split: lowerBodySplit, exerciseName: "Leg Curl", order: 2, sets: 3, reps: 12, weight: 90)
 | 
			
		||||
        let adductor = Exercise(split: lowerBodySplit, exerciseName: "Adductor", order: 3, sets: 3, reps: 15, weight: 100)
 | 
			
		||||
        let abductor = Exercise(split: lowerBodySplit, exerciseName: "Abductor", order: 4, sets: 3, reps: 15, weight: 90)
 | 
			
		||||
        let calfs = Exercise(split: lowerBodySplit, exerciseName: "Calfs", order: 5, sets: 3, reps: 15, weight: 120)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(legPress)
 | 
			
		||||
        context.insert(legExtension)
 | 
			
		||||
        context.insert(legCurl)
 | 
			
		||||
        context.insert(adductor)
 | 
			
		||||
        context.insert(abductor)
 | 
			
		||||
        context.insert(calfs)
 | 
			
		||||
        
 | 
			
		||||
        // Full Body Exercises (selected from both upper and lower)
 | 
			
		||||
        let fullBodyChestPress = Exercise(split: fullBodySplit, exerciseName: "Chest Press", order: 0, sets: 3, reps: 10, weight: 130)
 | 
			
		||||
        let fullBodyLatPullDown = Exercise(split: fullBodySplit, exerciseName: "Lat Pull Down", order: 1, sets: 3, reps: 12, weight: 120)
 | 
			
		||||
        let fullBodyLegPress = Exercise(split: fullBodySplit, exerciseName: "Leg Press", order: 2, sets: 3, reps: 12, weight: 200)
 | 
			
		||||
        let fullBodyAbdominal = Exercise(split: fullBodySplit, exerciseName: "Abdominal", order: 3, sets: 3, reps: 15, weight: 80)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(fullBodyChestPress)
 | 
			
		||||
        context.insert(fullBodyLatPullDown)
 | 
			
		||||
        context.insert(fullBodyLegPress)
 | 
			
		||||
        context.insert(fullBodyAbdominal)
 | 
			
		||||
        
 | 
			
		||||
        // Create workouts
 | 
			
		||||
        let now = Date()
 | 
			
		||||
        
 | 
			
		||||
        // Upper Body Workout (in progress)
 | 
			
		||||
        let upperBodyWorkout = Workout(start: now, end: now, split: upperBodySplit)
 | 
			
		||||
        upperBodyWorkout.status = .inProgress
 | 
			
		||||
        upperBodyWorkout.end = nil
 | 
			
		||||
        context.insert(upperBodyWorkout)
 | 
			
		||||
        
 | 
			
		||||
        // Lower Body Workout (scheduled for tomorrow)
 | 
			
		||||
        let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now) ?? now
 | 
			
		||||
        let lowerBodyWorkout = Workout(start: tomorrow, end: tomorrow, split: lowerBodySplit)
 | 
			
		||||
        lowerBodyWorkout.status = .notStarted
 | 
			
		||||
        context.insert(lowerBodyWorkout)
 | 
			
		||||
        
 | 
			
		||||
        // Full Body Workout (completed yesterday)
 | 
			
		||||
        let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: now) ?? now
 | 
			
		||||
        let fullBodyWorkout = Workout(start: yesterday, end: yesterday, split: fullBodySplit)
 | 
			
		||||
        fullBodyWorkout.status = .completed
 | 
			
		||||
        context.insert(fullBodyWorkout)
 | 
			
		||||
        
 | 
			
		||||
        // Create workout logs for Upper Body workout (in progress)
 | 
			
		||||
        let chestPressLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: chestPress.name, date: now, order: 0, sets: chestPress.sets, reps: chestPress.reps, weight: chestPress.weight, status: .completed, completed: true)
 | 
			
		||||
        let shoulderPressLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: shoulderPress.name, date: now, order: 1, sets: shoulderPress.sets, reps: shoulderPress.reps, weight: shoulderPress.weight, status: .completed, completed: true)
 | 
			
		||||
        let latPullDownLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: latPullDown.name, date: now, order: 2, sets: latPullDown.sets, reps: latPullDown.reps, weight: latPullDown.weight, status: .inProgress, completed: false)
 | 
			
		||||
        let seatedRowLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: seatedRow.name, date: now, order: 3, sets: seatedRow.sets, reps: seatedRow.reps, weight: seatedRow.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let tricepPressLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: tricepPress.name, date: now, order: 4, sets: tricepPress.sets, reps: tricepPress.reps, weight: tricepPress.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let armCurlLog = WorkoutLog(workout: upperBodyWorkout, exerciseName: armCurl.name, date: now, order: 5, sets: armCurl.sets, reps: armCurl.reps, weight: armCurl.weight, status: .notStarted, completed: false)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(chestPressLog)
 | 
			
		||||
        context.insert(shoulderPressLog)
 | 
			
		||||
        context.insert(latPullDownLog)
 | 
			
		||||
        context.insert(seatedRowLog)
 | 
			
		||||
        context.insert(tricepPressLog)
 | 
			
		||||
        context.insert(armCurlLog)
 | 
			
		||||
        
 | 
			
		||||
        // Create workout logs for Lower Body workout (scheduled)
 | 
			
		||||
        let legPressLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: legPress.name, date: tomorrow, order: 0, sets: legPress.sets, reps: legPress.reps, weight: legPress.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let legExtensionLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: legExtension.name, date: tomorrow, order: 1, sets: legExtension.sets, reps: legExtension.reps, weight: legExtension.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let legCurlLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: legCurl.name, date: tomorrow, order: 2, sets: legCurl.sets, reps: legCurl.reps, weight: legCurl.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let adductorLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: adductor.name, date: tomorrow, order: 3, sets: adductor.sets, reps: adductor.reps, weight: adductor.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let abductorLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: abductor.name, date: tomorrow, order: 4, sets: abductor.sets, reps: abductor.reps, weight: abductor.weight, status: .notStarted, completed: false)
 | 
			
		||||
        let calfsLog = WorkoutLog(workout: lowerBodyWorkout, exerciseName: calfs.name, date: tomorrow, order: 5, sets: calfs.sets, reps: calfs.reps, weight: calfs.weight, status: .notStarted, completed: false)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(legPressLog)
 | 
			
		||||
        context.insert(legExtensionLog)
 | 
			
		||||
        context.insert(legCurlLog)
 | 
			
		||||
        context.insert(adductorLog)
 | 
			
		||||
        context.insert(abductorLog)
 | 
			
		||||
        context.insert(calfsLog)
 | 
			
		||||
        
 | 
			
		||||
        // Create workout logs for Full Body workout (completed)
 | 
			
		||||
        let fullBodyChestPressLog = WorkoutLog(workout: fullBodyWorkout, exerciseName: fullBodyChestPress.name, date: yesterday, order: 0, sets: fullBodyChestPress.sets, reps: fullBodyChestPress.reps, weight: fullBodyChestPress.weight, status: .completed, completed: true)
 | 
			
		||||
        let fullBodyLatPullDownLog = WorkoutLog(workout: fullBodyWorkout, exerciseName: fullBodyLatPullDown.name, date: yesterday, order: 1, sets: fullBodyLatPullDown.sets, reps: fullBodyLatPullDown.reps, weight: fullBodyLatPullDown.weight, status: .completed, completed: true)
 | 
			
		||||
        let fullBodyLegPressLog = WorkoutLog(workout: fullBodyWorkout, exerciseName: fullBodyLegPress.name, date: yesterday, order: 2, sets: fullBodyLegPress.sets, reps: fullBodyLegPress.reps, weight: fullBodyLegPress.weight, status: .completed, completed: true)
 | 
			
		||||
        let fullBodyAbdominalLog = WorkoutLog(workout: fullBodyWorkout, exerciseName: fullBodyAbdominal.name, date: yesterday, order: 3, sets: fullBodyAbdominal.sets, reps: fullBodyAbdominal.reps, weight: fullBodyAbdominal.weight, status: .completed, completed: true)
 | 
			
		||||
        
 | 
			
		||||
        context.insert(fullBodyChestPressLog)
 | 
			
		||||
        context.insert(fullBodyLatPullDownLog)
 | 
			
		||||
        context.insert(fullBodyLegPressLog)
 | 
			
		||||
        context.insert(fullBodyAbdominalLog)
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            try context.save()
 | 
			
		||||
            logger.info("Successfully populated simulator database with test data from pf-starter-exercises.yaml")
 | 
			
		||||
        } catch {
 | 
			
		||||
            logger.error("Failed to save test data: \(error.localizedDescription)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								Worksouts Watch App/Schema/SchemaVersion.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Worksouts Watch App/Schema/SchemaVersion.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
enum SchemaVersion {
 | 
			
		||||
    static var models: [any PersistentModel.Type] = [
 | 
			
		||||
        Split.self,
 | 
			
		||||
        Exercise.self,
 | 
			
		||||
        Workout.self,
 | 
			
		||||
        WorkoutLog.self
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								Worksouts Watch App/Utils/AppLogger.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Worksouts Watch App/Utils/AppLogger.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import OSLog
 | 
			
		||||
 | 
			
		||||
struct AppLogger {
 | 
			
		||||
    private let logger: Logger
 | 
			
		||||
    
 | 
			
		||||
    init(subsystem: String, category: String) {
 | 
			
		||||
        self.logger = Logger(subsystem: subsystem, category: category)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func debug(_ message: String) {
 | 
			
		||||
        logger.debug("\(message)")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func info(_ message: String) {
 | 
			
		||||
        logger.info("\(message)")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func error(_ message: String) {
 | 
			
		||||
        logger.error("\(message)")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								Worksouts Watch App/Utils/Color+color.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Worksouts Watch App/Utils/Color+color.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
extension Color {
 | 
			
		||||
    static func color(from name: String) -> Color {
 | 
			
		||||
        switch name {
 | 
			
		||||
        case "red": return .red
 | 
			
		||||
        case "orange": return .orange
 | 
			
		||||
        case "yellow": return .yellow
 | 
			
		||||
        case "green": return .green
 | 
			
		||||
        case "mint": return .mint
 | 
			
		||||
        case "teal": return .teal
 | 
			
		||||
        case "cyan": return .cyan
 | 
			
		||||
        case "blue": return .blue
 | 
			
		||||
        case "indigo": return .indigo
 | 
			
		||||
        case "purple": return .purple
 | 
			
		||||
        case "pink": return .pink
 | 
			
		||||
        case "brown": return .brown
 | 
			
		||||
        default: return .indigo
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								Worksouts Watch App/Utils/Date+formatDate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Worksouts Watch App/Utils/Date+formatDate.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formatDate() -> String {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .short
 | 
			
		||||
        return formatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								Worksouts Watch App/Utils/Date+formatDateET.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Worksouts Watch App/Utils/Date+formatDateET.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formatDateET(format: String = "MMMM, d yyyy @ h:mm a z") -> String {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.timeZone = TimeZone(identifier: "America/New_York")
 | 
			
		||||
        formatter.dateFormat = format
 | 
			
		||||
        return formatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var ISO8601: String {
 | 
			
		||||
        "yyyy-MM-dd'T'HH:mm:ssZ"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								Worksouts Watch App/Utils/Date+formatedDate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Worksouts Watch App/Utils/Date+formatedDate.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    func formattedDate() -> String {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .short
 | 
			
		||||
        return formatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								Worksouts Watch App/Utils/HapticFeedback.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								Worksouts Watch App/Utils/HapticFeedback.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import WatchKit
 | 
			
		||||
 | 
			
		||||
struct HapticFeedback {
 | 
			
		||||
    static func success() {
 | 
			
		||||
        WKInterfaceDevice.current().play(.success)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static func notification() {
 | 
			
		||||
        WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static func click() {
 | 
			
		||||
        WKInterfaceDevice.current().play(.click)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static func doubleTap() {
 | 
			
		||||
        WKInterfaceDevice.current().play(.click)
 | 
			
		||||
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
 | 
			
		||||
            WKInterfaceDevice.current().play(.click)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static func tripleTap() {
 | 
			
		||||
        WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
 | 
			
		||||
            WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
        }
 | 
			
		||||
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
 | 
			
		||||
            WKInterfaceDevice.current().play(.notification)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										230
									
								
								Worksouts Watch App/Views/ActiveWorkoutListView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								Worksouts Watch App/Views/ActiveWorkoutListView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
//
 | 
			
		||||
// ActiveWorkoutListView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/20/25 at 6:35 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct ActiveWorkoutListView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    let workouts: [Workout]
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        List {
 | 
			
		||||
            ForEach(workouts) { workout in
 | 
			
		||||
                NavigationLink {
 | 
			
		||||
                    WorkoutDetailView(workout: workout)
 | 
			
		||||
                } label: {
 | 
			
		||||
                    WorkoutCardView(workout: workout)
 | 
			
		||||
                }
 | 
			
		||||
//                .listRowSeparator(.hidden)
 | 
			
		||||
                .listRowBackground(
 | 
			
		||||
                    RoundedRectangle(cornerRadius: 12)
 | 
			
		||||
                        .fill(Color.secondary.opacity(0.2))
 | 
			
		||||
                        .padding(
 | 
			
		||||
                            EdgeInsets(
 | 
			
		||||
                                top: 4,
 | 
			
		||||
                                leading: 8,
 | 
			
		||||
                                bottom: 4,
 | 
			
		||||
                                trailing: 8
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .listStyle(.carousel)
 | 
			
		||||
//        .navigationTitle("Workouts")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WorkoutCardView: View {
 | 
			
		||||
    let workout: Workout
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(alignment: .leading) {
 | 
			
		||||
            // Split icon
 | 
			
		||||
            if let split = workout.split {
 | 
			
		||||
                Image(systemName: split.systemImage)
 | 
			
		||||
                    .font(.system(size: 48))
 | 
			
		||||
                    .foregroundStyle(split.getColor())
 | 
			
		||||
            } else {
 | 
			
		||||
                Image(systemName: "dumbbell.fill")
 | 
			
		||||
                    .font(.system(size: 24))
 | 
			
		||||
                    .foregroundStyle(.gray)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
//            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                // Split name
 | 
			
		||||
                Text(workout.split?.name ?? "Workout")
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
                    .foregroundStyle(.white)
 | 
			
		||||
                
 | 
			
		||||
                // Workout status
 | 
			
		||||
                Text(workout.status?.name ?? "Not Started")
 | 
			
		||||
                    .font(.caption)
 | 
			
		||||
                    .foregroundStyle(Color.accentColor)
 | 
			
		||||
//            }
 | 
			
		||||
            
 | 
			
		||||
//            Spacer()
 | 
			
		||||
        }
 | 
			
		||||
//        .padding(.vertical, 8)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WorkoutDetailView: View {
 | 
			
		||||
    let workout: Workout
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(alignment: .center, spacing: 8) {
 | 
			
		||||
            if let logs = workout.logs?.sorted(by: { $0.order < $1.order }), !logs.isEmpty {
 | 
			
		||||
                List {
 | 
			
		||||
                    ForEach(logs) { log in
 | 
			
		||||
                        NavigationLink {
 | 
			
		||||
                            WorkoutLogDetailView(log: log)
 | 
			
		||||
                        } label: {
 | 
			
		||||
                            WorkoutLogCardView(log: log)
 | 
			
		||||
                        }
 | 
			
		||||
                        .listRowBackground(
 | 
			
		||||
                            RoundedRectangle(cornerRadius: 12)
 | 
			
		||||
                                .fill(Color.secondary.opacity(0.2))
 | 
			
		||||
                                .padding(
 | 
			
		||||
                                    EdgeInsets(
 | 
			
		||||
                                        top: 4,
 | 
			
		||||
                                        leading: 8,
 | 
			
		||||
                                        bottom: 4,
 | 
			
		||||
                                        trailing: 8
 | 
			
		||||
                                    )
 | 
			
		||||
                                )
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .listStyle(.carousel)
 | 
			
		||||
            } else {
 | 
			
		||||
                Text("No exercises in this workout")
 | 
			
		||||
                    .font(.body)
 | 
			
		||||
                    .foregroundStyle(.secondary)
 | 
			
		||||
                    .padding()
 | 
			
		||||
                
 | 
			
		||||
                Spacer()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WorkoutLogCardView: View {
 | 
			
		||||
    let log: WorkoutLog
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(alignment: .leading, spacing: 8) {
 | 
			
		||||
            // Exercise name
 | 
			
		||||
            Text(log.exerciseName)
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .lineLimit(1)
 | 
			
		||||
            
 | 
			
		||||
            // Status
 | 
			
		||||
            Text(log.status?.name ?? "Not Started")
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
                .foregroundStyle(Color.accentColor)
 | 
			
		||||
            
 | 
			
		||||
            // Sets, Reps, Weight
 | 
			
		||||
            HStack(spacing: 12) {
 | 
			
		||||
                Text("\(log.weight) lbs")
 | 
			
		||||
                
 | 
			
		||||
                Spacer()
 | 
			
		||||
                
 | 
			
		||||
                Text("\(log.sets) × \(log.reps)")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WorkoutLogDetailView: View {
 | 
			
		||||
    let log: WorkoutLog
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(spacing: 16) {
 | 
			
		||||
            Text(log.exerciseName)
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
            
 | 
			
		||||
            VStack(alignment: .leading, spacing: 8) {
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Text("Sets:")
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    Text("\(log.sets)")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Text("Reps:")
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    Text("\(log.reps)")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Text("Weight:")
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    Text("\(log.weight)")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Text("Status:")
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    Text(log.status?.name ?? "Not Started")
 | 
			
		||||
                        .foregroundStyle(statusColor(for: log.status))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding()
 | 
			
		||||
            .background(Color.secondary.opacity(0.1))
 | 
			
		||||
            .cornerRadius(10)
 | 
			
		||||
            
 | 
			
		||||
            NavigationLink {
 | 
			
		||||
                ExerciseProgressControlView(log: log)
 | 
			
		||||
            } label: {
 | 
			
		||||
                Text("Start Exercise")
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(.borderedProminent)
 | 
			
		||||
            .tint(.accentColor)
 | 
			
		||||
            
 | 
			
		||||
            Spacer()
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func statusColor(for status: WorkoutStatus?) -> Color {
 | 
			
		||||
        guard let status = status else { return .secondary }
 | 
			
		||||
        
 | 
			
		||||
        switch status {
 | 
			
		||||
        case .notStarted:
 | 
			
		||||
            return .secondary
 | 
			
		||||
        case .inProgress:
 | 
			
		||||
            return .blue
 | 
			
		||||
        case .completed:
 | 
			
		||||
            return .green
 | 
			
		||||
        case .skipped:
 | 
			
		||||
            return .red
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    let container = AppContainer.preview
 | 
			
		||||
//    let split = Split(name: "Upper Body", color: "blue", systemImage: "figure.strengthtraining.traditional")
 | 
			
		||||
//    let workout1 = Workout(start: Date(), end: nil, split: split)
 | 
			
		||||
//    workout1.status = .inProgress
 | 
			
		||||
//    
 | 
			
		||||
//    let split2 = Split(name: "Lower Body", color: "red", systemImage: "figure.run")
 | 
			
		||||
//    let workout2 = Workout(start: Date().addingTimeInterval(-3600), end: nil, split: split2)
 | 
			
		||||
//    workout2.status = .notStarted
 | 
			
		||||
//    
 | 
			
		||||
//    return ActiveWorkoutListView(workouts: [workout1, workout2])
 | 
			
		||||
//        .modelContainer(container)
 | 
			
		||||
//}
 | 
			
		||||
							
								
								
									
										268
									
								
								Worksouts Watch App/Views/ExerciseProgressControlView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								Worksouts Watch App/Views/ExerciseProgressControlView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,268 @@
 | 
			
		||||
//
 | 
			
		||||
// ExerciseProgressControlView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/20/25 at 7:19 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
enum ExerciseState: Identifiable {
 | 
			
		||||
    case set(number: Int)
 | 
			
		||||
    case rest(afterSet: Int)
 | 
			
		||||
    case done
 | 
			
		||||
    
 | 
			
		||||
    var id: String {
 | 
			
		||||
        switch self {
 | 
			
		||||
        case .set(let number):
 | 
			
		||||
            return "set_\(number)"
 | 
			
		||||
        case .rest(let afterSet):
 | 
			
		||||
            return "rest_\(afterSet)"
 | 
			
		||||
        case .done:
 | 
			
		||||
            return "done"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var isRest: Bool {
 | 
			
		||||
        if case .rest = self {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var isSet: Bool {
 | 
			
		||||
        if case .set = self {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var isDone: Bool {
 | 
			
		||||
        if case .done = self {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExerciseProgressControlView: View {
 | 
			
		||||
    let log: WorkoutLog
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    
 | 
			
		||||
    @State private var exerciseStates: [ExerciseState] = []
 | 
			
		||||
    @State private var currentStateIndex: Int = 0
 | 
			
		||||
    @State private var elapsedSeconds: Int = 0
 | 
			
		||||
    @State private var timer: Timer? = nil
 | 
			
		||||
    @State private var previousStateIndex: Int = 0
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        TabView(selection: $currentStateIndex) {
 | 
			
		||||
            ForEach(Array(exerciseStates.enumerated()), id: \.element.id) { index, state in
 | 
			
		||||
                ExerciseStateView(
 | 
			
		||||
                    state: state,
 | 
			
		||||
                    elapsedSeconds: elapsedSeconds,
 | 
			
		||||
                    onComplete: {
 | 
			
		||||
                        moveToNextState()
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                .tag(index)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .tabViewStyle(.page(indexDisplayMode: .never))
 | 
			
		||||
        .onChange(of: currentStateIndex) { oldValue, newValue in
 | 
			
		||||
            if oldValue != newValue {
 | 
			
		||||
                // Reset timer when user swipes to a new state
 | 
			
		||||
                elapsedSeconds = 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            setupExerciseStates()
 | 
			
		||||
            startTimer()
 | 
			
		||||
        }
 | 
			
		||||
        .onDisappear {
 | 
			
		||||
            stopTimer()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func setupExerciseStates() {
 | 
			
		||||
        var states: [ExerciseState] = []
 | 
			
		||||
        
 | 
			
		||||
        // Create states for each set and rest period
 | 
			
		||||
        for setNumber in 1...log.sets {
 | 
			
		||||
            states.append(.set(number: setNumber))
 | 
			
		||||
            
 | 
			
		||||
            // Add rest period after each set except the last one
 | 
			
		||||
            if setNumber < log.sets {
 | 
			
		||||
                states.append(.rest(afterSet: setNumber))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Add done state at the end
 | 
			
		||||
        states.append(.done)
 | 
			
		||||
        
 | 
			
		||||
        exerciseStates = states
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func startTimer() {
 | 
			
		||||
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
 | 
			
		||||
            elapsedSeconds += 1
 | 
			
		||||
            
 | 
			
		||||
            // Check if we need to provide haptic feedback during rest periods
 | 
			
		||||
            if let currentState = exerciseStates[safe: currentStateIndex] {
 | 
			
		||||
                if currentState.isRest {
 | 
			
		||||
                    provideRestHapticFeedback()
 | 
			
		||||
                } else if currentState.isDone && elapsedSeconds >= 10 {
 | 
			
		||||
                    // Auto-complete after 10 seconds on the DONE state
 | 
			
		||||
                    completeExercise()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func stopTimer() {
 | 
			
		||||
        timer?.invalidate()
 | 
			
		||||
        timer = nil
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func moveToNextState() {
 | 
			
		||||
        if currentStateIndex < exerciseStates.count - 1 {
 | 
			
		||||
            withAnimation {
 | 
			
		||||
                currentStateIndex += 1
 | 
			
		||||
                elapsedSeconds = 0
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // We've reached the end (DONE state)
 | 
			
		||||
            completeExercise()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func provideRestHapticFeedback() {
 | 
			
		||||
        // Provide haptic feedback based on elapsed time
 | 
			
		||||
        if elapsedSeconds % 60 == 0 && elapsedSeconds > 0 {
 | 
			
		||||
            // Triple tap every 60 seconds
 | 
			
		||||
            HapticFeedback.tripleTap()
 | 
			
		||||
        } else if elapsedSeconds % 30 == 0 && elapsedSeconds > 0 {
 | 
			
		||||
            // Double tap every 30 seconds
 | 
			
		||||
            HapticFeedback.doubleTap()
 | 
			
		||||
        } else if elapsedSeconds % 10 == 0 && elapsedSeconds > 0 {
 | 
			
		||||
            // Single tap every 10 seconds
 | 
			
		||||
            HapticFeedback.success()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func completeExercise() {
 | 
			
		||||
        // Update the workout log status to completed
 | 
			
		||||
        log.status = .completed
 | 
			
		||||
        
 | 
			
		||||
        // Provide "tada" haptic feedback
 | 
			
		||||
        HapticFeedback.tripleTap()
 | 
			
		||||
        
 | 
			
		||||
        // Dismiss this view to return to WorkoutDetailView
 | 
			
		||||
        dismiss()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExerciseStateView: View {
 | 
			
		||||
    let state: ExerciseState
 | 
			
		||||
    let elapsedSeconds: Int
 | 
			
		||||
    let onComplete: () -> Void
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(spacing: 20) {
 | 
			
		||||
            // Title based on state
 | 
			
		||||
            Text(stateTitle)
 | 
			
		||||
                .font(.title3)
 | 
			
		||||
                .fontWeight(.bold)
 | 
			
		||||
            
 | 
			
		||||
            // Timer display
 | 
			
		||||
            Text(timeFormatted)
 | 
			
		||||
                .font(.system(size: 48, weight: .semibold, design: .monospaced))
 | 
			
		||||
                .foregroundStyle(state.isRest ? .orange : .accentColor)
 | 
			
		||||
            
 | 
			
		||||
            // Only show Done button and countdown for the final state
 | 
			
		||||
            if state.isDone {
 | 
			
		||||
                // Countdown message
 | 
			
		||||
                if elapsedSeconds < 10 {
 | 
			
		||||
                    Text("Completing automatically in \(10 - elapsedSeconds) seconds")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .multilineTextAlignment(.center)
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Text("Auto-completing...")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Done button
 | 
			
		||||
                Button(action: onComplete) {
 | 
			
		||||
                    Text("Done")
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                        .frame(maxWidth: .infinity)
 | 
			
		||||
                }
 | 
			
		||||
                .buttonStyle(.borderedProminent)
 | 
			
		||||
                .tint(.green)
 | 
			
		||||
                .padding(.horizontal)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var stateTitle: String {
 | 
			
		||||
        switch state {
 | 
			
		||||
        case .set(let number):
 | 
			
		||||
            return "Set \(number) in progress"
 | 
			
		||||
        case .rest:
 | 
			
		||||
            return "Resting"
 | 
			
		||||
        case .done:
 | 
			
		||||
            return "Exercise Complete"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var buttonTitle: String {
 | 
			
		||||
        switch state {
 | 
			
		||||
        case .set:
 | 
			
		||||
            return "Complete Set"
 | 
			
		||||
        case .rest:
 | 
			
		||||
            return "Start Next Set"
 | 
			
		||||
        case .done:
 | 
			
		||||
            return "DONE"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var buttonColor: Color {
 | 
			
		||||
        switch state {
 | 
			
		||||
        case .set:
 | 
			
		||||
            return .accentColor
 | 
			
		||||
        case .rest:
 | 
			
		||||
            return .orange
 | 
			
		||||
        case .done:
 | 
			
		||||
            return .green
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var timeFormatted: String {
 | 
			
		||||
        let minutes = elapsedSeconds / 60
 | 
			
		||||
        let seconds = elapsedSeconds % 60
 | 
			
		||||
        return String(format: "%02d:%02d", minutes, seconds)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extension to safely access array elements
 | 
			
		||||
extension Array {
 | 
			
		||||
    subscript(safe index: Index) -> Element? {
 | 
			
		||||
        return indices.contains(index) ? self[index] : nil
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    let container = AppContainer.preview
 | 
			
		||||
//    let workout = Workout(start: Date(), end: nil, split: nil)
 | 
			
		||||
//    let log = WorkoutLog(workout: workout, exerciseName: "Bench Press", date: Date(), sets: 3, reps: 10, weight: 135)
 | 
			
		||||
//    
 | 
			
		||||
//    ExerciseProgressControlView(log: log)
 | 
			
		||||
//        .modelContainer(container)
 | 
			
		||||
//}
 | 
			
		||||
							
								
								
									
										317
									
								
								Worksouts Watch App/Views/ExerciseProgressView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								Worksouts Watch App/Views/ExerciseProgressView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,317 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
import WatchKit
 | 
			
		||||
 | 
			
		||||
// Enum to track the current phase of the exercise
 | 
			
		||||
enum ExercisePhase {
 | 
			
		||||
    case notStarted
 | 
			
		||||
    case exercising(setNumber: Int)
 | 
			
		||||
    case resting(setNumber: Int, elapsedSeconds: Int)
 | 
			
		||||
    case completed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExerciseProgressView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    
 | 
			
		||||
    let log: WorkoutLog
 | 
			
		||||
    
 | 
			
		||||
    @State private var phase: ExercisePhase = .notStarted
 | 
			
		||||
    @State private var hapticSeconds: Int = 0
 | 
			
		||||
    @State private var restSeconds: Int = 0
 | 
			
		||||
    @State private var hapticTimer: Timer? = nil
 | 
			
		||||
    @State private var restTimer: Timer? = nil
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        ScrollView {
 | 
			
		||||
            VStack(spacing: 15) {
 | 
			
		||||
                exerciseHeader
 | 
			
		||||
                
 | 
			
		||||
                switch phase {
 | 
			
		||||
                case .notStarted:
 | 
			
		||||
                    startPhaseView
 | 
			
		||||
                case .exercising(let setNumber):
 | 
			
		||||
                    exercisingPhaseView(setNumber: setNumber)
 | 
			
		||||
                case .resting(let setNumber, let elapsedSeconds):
 | 
			
		||||
                    restingPhaseView(setNumber: setNumber, elapsedSeconds: elapsedSeconds)
 | 
			
		||||
                case .completed:
 | 
			
		||||
                    completedPhaseView
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding()
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle(log.exerciseName)
 | 
			
		||||
        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
        .onDisappear {
 | 
			
		||||
            stopTimers()
 | 
			
		||||
        }
 | 
			
		||||
        .gesture(
 | 
			
		||||
            DragGesture(minimumDistance: 50)
 | 
			
		||||
                .onEnded { gesture in
 | 
			
		||||
                    if gesture.translation.width < 0 {
 | 
			
		||||
                        // Swipe left - progress to next phase
 | 
			
		||||
                        handleSwipeLeft()
 | 
			
		||||
                    } else if gesture.translation.height < 0 && gesture.translation.height < -50 {
 | 
			
		||||
                        // Swipe up - cancel current set
 | 
			
		||||
                        handleSwipeUp()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // MARK: - View Components
 | 
			
		||||
    
 | 
			
		||||
    private var exerciseHeader: some View {
 | 
			
		||||
        VStack(alignment: .leading, spacing: 5) {
 | 
			
		||||
            Text(log.exerciseName)
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .foregroundColor(.primary)
 | 
			
		||||
            
 | 
			
		||||
            Text("\(log.sets) sets × \(log.reps) reps")
 | 
			
		||||
                .font(.subheadline)
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
            
 | 
			
		||||
            Text("\(log.weight) lbs")
 | 
			
		||||
                .font(.subheadline)
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
        }
 | 
			
		||||
        .frame(maxWidth: .infinity, alignment: .leading)
 | 
			
		||||
        .padding(.bottom, 5)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var startPhaseView: some View {
 | 
			
		||||
        VStack(spacing: 20) {
 | 
			
		||||
            Text("Ready to start?")
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
            
 | 
			
		||||
            Button(action: startFirstSet) {
 | 
			
		||||
                Text("Start First Set")
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(.borderedProminent)
 | 
			
		||||
            .tint(.blue)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func exercisingPhaseView(setNumber: Int) -> some View {
 | 
			
		||||
        VStack(spacing: 20) {
 | 
			
		||||
            Text("Set \(setNumber) of \(log.sets)")
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
            
 | 
			
		||||
            Text("Exercising...")
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
            
 | 
			
		||||
            HStack(spacing: 20) {
 | 
			
		||||
                Button(action: completeSet) {
 | 
			
		||||
                    Text("Complete")
 | 
			
		||||
                        .frame(maxWidth: .infinity)
 | 
			
		||||
                }
 | 
			
		||||
                .buttonStyle(.borderedProminent)
 | 
			
		||||
                .tint(.green)
 | 
			
		||||
                
 | 
			
		||||
                Button(action: cancelSet) {
 | 
			
		||||
                    Text("Cancel")
 | 
			
		||||
                        .frame(maxWidth: .infinity)
 | 
			
		||||
                }
 | 
			
		||||
                .buttonStyle(.bordered)
 | 
			
		||||
                .tint(.red)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Text("Or swipe left to complete")
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
            
 | 
			
		||||
            Text("Swipe up to cancel")
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func restingPhaseView(setNumber: Int, elapsedSeconds: Int) -> some View {
 | 
			
		||||
        VStack(spacing: 20) {
 | 
			
		||||
            Text("Rest after Set \(setNumber)")
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
            
 | 
			
		||||
            Text("Rest time: \(formatSeconds(elapsedSeconds))")
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
            
 | 
			
		||||
            if setNumber < (log.sets) {
 | 
			
		||||
                Button(action: { startNextSet(after: setNumber) }) {
 | 
			
		||||
                    Text("Start Set \(setNumber + 1)")
 | 
			
		||||
                        .frame(maxWidth: .infinity)
 | 
			
		||||
                }
 | 
			
		||||
                .buttonStyle(.borderedProminent)
 | 
			
		||||
                .tint(.blue)
 | 
			
		||||
                
 | 
			
		||||
                Text("Or swipe left to start next set")
 | 
			
		||||
                    .font(.caption)
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
            } else {
 | 
			
		||||
                Button(action: completeExercise) {
 | 
			
		||||
                    Text("Complete Exercise")
 | 
			
		||||
                        .frame(maxWidth: .infinity)
 | 
			
		||||
                }
 | 
			
		||||
                .buttonStyle(.borderedProminent)
 | 
			
		||||
                .tint(.green)
 | 
			
		||||
                
 | 
			
		||||
                Text("Or swipe left to complete")
 | 
			
		||||
                    .font(.caption)
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private var completedPhaseView: some View {
 | 
			
		||||
        VStack(spacing: 20) {
 | 
			
		||||
            Text("Exercise Completed!")
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .foregroundColor(.green)
 | 
			
		||||
            
 | 
			
		||||
            Image(systemName: "checkmark.circle.fill")
 | 
			
		||||
                .font(.system(size: 50))
 | 
			
		||||
                .foregroundColor(.green)
 | 
			
		||||
            
 | 
			
		||||
            Button(action: { dismiss() }) {
 | 
			
		||||
                Text("Return to Workout")
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(.borderedProminent)
 | 
			
		||||
            .tint(.blue)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // MARK: - Action Handlers
 | 
			
		||||
    
 | 
			
		||||
    private func handleSwipeLeft() {
 | 
			
		||||
        switch phase {
 | 
			
		||||
        case .notStarted:
 | 
			
		||||
            startFirstSet()
 | 
			
		||||
        case .exercising:
 | 
			
		||||
            completeSet()
 | 
			
		||||
        case .resting(let setNumber, _):
 | 
			
		||||
            if setNumber < (log.sets) {
 | 
			
		||||
                startNextSet(after: setNumber)
 | 
			
		||||
            } else {
 | 
			
		||||
                completeExercise()
 | 
			
		||||
            }
 | 
			
		||||
        case .completed:
 | 
			
		||||
            dismiss()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func handleSwipeUp() {
 | 
			
		||||
        if case .exercising = phase {
 | 
			
		||||
            cancelSet()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func startFirstSet() {
 | 
			
		||||
        phase = .exercising(setNumber: 1)
 | 
			
		||||
        startHapticTimer()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func startNextSet(after completedSetNumber: Int) {
 | 
			
		||||
        stopTimers()
 | 
			
		||||
        let nextSetNumber = completedSetNumber + 1
 | 
			
		||||
        phase = .exercising(setNumber: nextSetNumber)
 | 
			
		||||
        startHapticTimer()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func completeSet() {
 | 
			
		||||
        stopTimers()
 | 
			
		||||
        
 | 
			
		||||
        if case .exercising(let setNumber) = phase {
 | 
			
		||||
            // Start rest timer
 | 
			
		||||
            phase = .resting(setNumber: setNumber, elapsedSeconds: 0)
 | 
			
		||||
            startRestTimer()
 | 
			
		||||
            startHapticTimer()
 | 
			
		||||
            
 | 
			
		||||
            // Play completion haptic
 | 
			
		||||
            HapticFeedback.success()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func cancelSet() {
 | 
			
		||||
        // Just go back to the previous state
 | 
			
		||||
        stopTimers()
 | 
			
		||||
        phase = .notStarted
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func completeExercise() {
 | 
			
		||||
        stopTimers()
 | 
			
		||||
        
 | 
			
		||||
        // Update workout log
 | 
			
		||||
        log.completed = true
 | 
			
		||||
        log.status = .completed
 | 
			
		||||
        try? modelContext.save()
 | 
			
		||||
        
 | 
			
		||||
        // Show completion screen
 | 
			
		||||
        phase = .completed
 | 
			
		||||
        
 | 
			
		||||
        // Play completion haptic
 | 
			
		||||
        HapticFeedback.success()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // MARK: - Timer Management
 | 
			
		||||
    
 | 
			
		||||
    private func startHapticTimer() {
 | 
			
		||||
        hapticSeconds = 0
 | 
			
		||||
        hapticTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
 | 
			
		||||
            hapticSeconds += 1
 | 
			
		||||
            
 | 
			
		||||
            // Provide haptic feedback based on time intervals
 | 
			
		||||
            if hapticSeconds % 60 == 0 {
 | 
			
		||||
                // Triple tap every 60 seconds
 | 
			
		||||
                HapticFeedback.tripleTap()
 | 
			
		||||
            } else if hapticSeconds % 30 == 0 {
 | 
			
		||||
                // Double tap every 30 seconds
 | 
			
		||||
                HapticFeedback.doubleTap()
 | 
			
		||||
            } else if hapticSeconds % 10 == 0 {
 | 
			
		||||
                // Light tap every 10 seconds
 | 
			
		||||
                HapticFeedback.click()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func startRestTimer() {
 | 
			
		||||
        restSeconds = 0
 | 
			
		||||
        restTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
 | 
			
		||||
            restSeconds += 1
 | 
			
		||||
            
 | 
			
		||||
            if case .resting(let setNumber, _) = phase {
 | 
			
		||||
                phase = .resting(setNumber: setNumber, elapsedSeconds: restSeconds)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func stopTimers() {
 | 
			
		||||
        hapticTimer?.invalidate()
 | 
			
		||||
        hapticTimer = nil
 | 
			
		||||
        
 | 
			
		||||
        restTimer?.invalidate()
 | 
			
		||||
        restTimer = nil
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // MARK: - Helper Functions
 | 
			
		||||
    
 | 
			
		||||
    private func formatSeconds(_ seconds: Int) -> String {
 | 
			
		||||
        let minutes = seconds / 60
 | 
			
		||||
        let remainingSeconds = seconds % 60
 | 
			
		||||
        return String(format: "%d:%02d", minutes, remainingSeconds)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    let config = ModelConfiguration(isStoredInMemoryOnly: true)
 | 
			
		||||
//    let container = try! ModelContainer(for: SchemaV1.models, configurations: config)
 | 
			
		||||
//    
 | 
			
		||||
//    // Create sample data
 | 
			
		||||
//    let exercise = Exercise(name: "Bench Press", sets: 3, reps: 8, weight: 60.0)
 | 
			
		||||
//    let workout = Workout(name: "Chest Day", date: Date())
 | 
			
		||||
//    let log = WorkoutLog(exercise: exercise, workout: workout)
 | 
			
		||||
//    
 | 
			
		||||
//    NavigationStack {
 | 
			
		||||
//        ExerciseProgressView(log: log)
 | 
			
		||||
//            .modelContainer(container)
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
							
								
								
									
										117
									
								
								Worksouts Watch App/Views/WorkoutLogListView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								Worksouts Watch App/Views/WorkoutLogListView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct WorkoutLogListView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    let workout: Workout
 | 
			
		||||
    
 | 
			
		||||
    @State private var selectedLogIndex: Int = 0
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            if let logs = workout.logs?.sorted(by: { $0.order < $1.order }), !logs.isEmpty {
 | 
			
		||||
                TabView(selection: $selectedLogIndex) {
 | 
			
		||||
                    ForEach(Array(logs.enumerated()), id: \.element.id) { index, log in
 | 
			
		||||
                        WorkoutLogCard(log: log, index: index)
 | 
			
		||||
                            .tag(index)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .tabViewStyle(.page)
 | 
			
		||||
//                .indexViewStyle(.page(backgroundDisplayMode: .always))
 | 
			
		||||
            } else {
 | 
			
		||||
                Text("No exercises in this workout")
 | 
			
		||||
                    .foregroundStyle(.secondary)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle(workout.split?.name ?? "Workout")
 | 
			
		||||
        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WorkoutLogCard: View {
 | 
			
		||||
    let log: WorkoutLog
 | 
			
		||||
    let index: Int
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(spacing: 12) {
 | 
			
		||||
            Text(log.exerciseName)
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .multilineTextAlignment(.center)
 | 
			
		||||
            
 | 
			
		||||
            HStack {
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("\(log.sets)")
 | 
			
		||||
                        .font(.title2)
 | 
			
		||||
                    Text("Sets")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
                .frame(maxWidth: .infinity)
 | 
			
		||||
                
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("\(log.reps)")
 | 
			
		||||
                        .font(.title2)
 | 
			
		||||
                    Text("Reps")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
                .frame(maxWidth: .infinity)
 | 
			
		||||
                
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("\(log.weight)")
 | 
			
		||||
                        .font(.title2)
 | 
			
		||||
                    Text("Weight")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundStyle(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
                .frame(maxWidth: .infinity)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            NavigationLink {
 | 
			
		||||
                ExerciseProgressView(log: log)
 | 
			
		||||
            } label: {
 | 
			
		||||
                Text("Start")
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
                    .foregroundStyle(.white)
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
                    .padding(.vertical, 8)
 | 
			
		||||
                    .background(Color.blue)
 | 
			
		||||
                    .cornerRadius(8)
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(PlainButtonStyle())
 | 
			
		||||
            
 | 
			
		||||
            Text(log.status?.name ?? "Not Started")
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
                .foregroundStyle(statusColor(for: log.status))
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
        .background(Color.secondary.opacity(0.2))
 | 
			
		||||
        .cornerRadius(12)
 | 
			
		||||
        .padding(.horizontal)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func statusColor(for status: WorkoutStatus?) -> Color {
 | 
			
		||||
        guard let status = status else { return .secondary }
 | 
			
		||||
        
 | 
			
		||||
        switch status {
 | 
			
		||||
        case .notStarted:
 | 
			
		||||
            return .secondary
 | 
			
		||||
        case .inProgress:
 | 
			
		||||
            return .blue
 | 
			
		||||
        case .completed:
 | 
			
		||||
            return .green
 | 
			
		||||
        case .skipped:
 | 
			
		||||
            return .red
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    let container = AppContainer.preview
 | 
			
		||||
    let workout = Workout(start: Date(), end: Date(), split: nil)
 | 
			
		||||
    let log1 = WorkoutLog(workout: workout, exerciseName: "Bench Press", date: Date(), sets: 3, reps: 10, weight: 135)
 | 
			
		||||
    let log2 = WorkoutLog(workout: workout, exerciseName: "Squats", date: Date(), order: 1, sets: 3, reps: 8, weight: 225)
 | 
			
		||||
    
 | 
			
		||||
    return WorkoutLogListView(workout: workout)
 | 
			
		||||
        .modelContainer(container)
 | 
			
		||||
}
 | 
			
		||||
@@ -2,19 +2,22 @@
 | 
			
		||||
// WorksoutsApp.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/15/25 at 7:09 PM.
 | 
			
		||||
// Created by rzen on 7/15/25 at 7:09 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
@main
 | 
			
		||||
struct Worksouts_Watch_AppApp: App {
 | 
			
		||||
    let container = AppContainer.create()
 | 
			
		||||
    
 | 
			
		||||
    var body: some Scene {
 | 
			
		||||
        WindowGroup {
 | 
			
		||||
            ContentView()
 | 
			
		||||
                .modelContainer(container)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user