wip
This commit is contained in:
		@@ -8,6 +8,7 @@
 | 
			
		||||
 | 
			
		||||
/* Begin PBXBuildFile section */
 | 
			
		||||
		A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 | 
			
		||||
		A45FA2732E29B12500581607 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = A45FA2722E29B12500581607 /* Yams */; };
 | 
			
		||||
/* End PBXBuildFile section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXContainerItemProxy section */
 | 
			
		||||
@@ -70,6 +71,7 @@
 | 
			
		||||
			isa = PBXFrameworksBuildPhase;
 | 
			
		||||
			buildActionMask = 2147483647;
 | 
			
		||||
			files = (
 | 
			
		||||
				A45FA2732E29B12500581607 /* Yams in Frameworks */,
 | 
			
		||||
			);
 | 
			
		||||
			runOnlyForDeploymentPostprocessing = 0;
 | 
			
		||||
		};
 | 
			
		||||
@@ -123,6 +125,7 @@
 | 
			
		||||
			);
 | 
			
		||||
			name = Workouts;
 | 
			
		||||
			packageProductDependencies = (
 | 
			
		||||
				A45FA2722E29B12500581607 /* Yams */,
 | 
			
		||||
			);
 | 
			
		||||
			productName = Workouts;
 | 
			
		||||
			productReference = A45FA0912E21B3DD00581607 /* Workouts.app */;
 | 
			
		||||
@@ -177,6 +180,9 @@
 | 
			
		||||
			);
 | 
			
		||||
			mainGroup = A45FA0882E21B3DC00581607;
 | 
			
		||||
			minimizedProjectReferenceProxies = 1;
 | 
			
		||||
			packageReferences = (
 | 
			
		||||
				A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */,
 | 
			
		||||
			);
 | 
			
		||||
			preferredProjectObjectVersion = 77;
 | 
			
		||||
			productRefGroup = A45FA0922E21B3DD00581607 /* Products */;
 | 
			
		||||
			projectDirPath = "";
 | 
			
		||||
@@ -505,6 +511,25 @@
 | 
			
		||||
			defaultConfigurationName = Release;
 | 
			
		||||
		};
 | 
			
		||||
/* End XCConfigurationList section */
 | 
			
		||||
 | 
			
		||||
/* Begin XCRemoteSwiftPackageReference section */
 | 
			
		||||
		A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */ = {
 | 
			
		||||
			isa = XCRemoteSwiftPackageReference;
 | 
			
		||||
			repositoryURL = "https://github.com/jpsim/Yams";
 | 
			
		||||
			requirement = {
 | 
			
		||||
				kind = upToNextMajorVersion;
 | 
			
		||||
				minimumVersion = 6.0.2;
 | 
			
		||||
			};
 | 
			
		||||
		};
 | 
			
		||||
/* End XCRemoteSwiftPackageReference section */
 | 
			
		||||
 | 
			
		||||
/* Begin XCSwiftPackageProductDependency section */
 | 
			
		||||
		A45FA2722E29B12500581607 /* Yams */ = {
 | 
			
		||||
			isa = XCSwiftPackageProductDependency;
 | 
			
		||||
			package = A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */;
 | 
			
		||||
			productName = Yams;
 | 
			
		||||
		};
 | 
			
		||||
/* End XCSwiftPackageProductDependency section */
 | 
			
		||||
	};
 | 
			
		||||
	rootObject = A45FA0892E21B3DC00581607 /* Project object */;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "originHash" : "81702b8f7fa6fc73458821cd4dfa7dde6684a9d1d4b6d63ecde0b6b832d8d75e",
 | 
			
		||||
  "pins" : [
 | 
			
		||||
    {
 | 
			
		||||
      "identity" : "yams",
 | 
			
		||||
      "kind" : "remoteSourceControl",
 | 
			
		||||
      "location" : "https://github.com/jpsim/Yams",
 | 
			
		||||
      "state" : {
 | 
			
		||||
        "revision" : "f4d4d6827d36092d151ad7f6fef1991c1b7192f6",
 | 
			
		||||
        "version" : "6.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "version" : 3
 | 
			
		||||
}
 | 
			
		||||
@@ -17,9 +17,14 @@ struct ContentView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationView {
 | 
			
		||||
            TabView {
 | 
			
		||||
                SplitsView()
 | 
			
		||||
                    .tabItem {
 | 
			
		||||
                        Label("Workouts", systemImage: "figure.strengthtraining.traditional")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                WorkoutsView()
 | 
			
		||||
                    .tabItem {
 | 
			
		||||
                        Label("Workout", systemImage: "figure.strengthtraining.traditional")
 | 
			
		||||
                        Label("Logs", systemImage: "list.bullet.clipboard.fill")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
 
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
@Model
 | 
			
		||||
final class Exercise {
 | 
			
		||||
    var name: String = ""
 | 
			
		||||
    var descr: String = ""
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \ExerciseType.exercises)
 | 
			
		||||
    var type: ExerciseType?
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \Muscle.exercises)
 | 
			
		||||
    var muscles: [Muscle]? = []
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \SplitExerciseAssignment.exercise)
 | 
			
		||||
    var splits: [SplitExerciseAssignment]? = []
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \WorkoutLog.exercise)
 | 
			
		||||
    var logs: [WorkoutLog]? = []
 | 
			
		||||
    
 | 
			
		||||
    init(name: String, descr: String) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.descr = descr
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static let unnamed = "Unnamed Exercise"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Exercise: EditableEntity {
 | 
			
		||||
    static func createNew() -> Exercise {
 | 
			
		||||
        return Exercise(name: "", descr: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
        return "Exercises"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    static func formView(for model: Exercise) -> some View {
 | 
			
		||||
        EntityAddEditView(model: model) { $model in
 | 
			
		||||
            // This internal view is necessary to use @Query within the form.
 | 
			
		||||
            ExerciseFormView(model: $model)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fileprivate struct ExerciseFormView: View {
 | 
			
		||||
    @Binding var model: Exercise
 | 
			
		||||
    @Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Section(header: Text("Name")) {
 | 
			
		||||
            TextField("Name", text: $model.name)
 | 
			
		||||
                .bold()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Exercise Type")) {
 | 
			
		||||
            Picker("Type", selection: $model.type) {
 | 
			
		||||
                Text("Select a type").tag(nil as ExerciseType?)
 | 
			
		||||
                ForEach(exerciseTypes) { type in
 | 
			
		||||
                    Text(type.name).tag(type as ExerciseType?)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Description")) {
 | 
			
		||||
            TextEditor(text: $model.descr)
 | 
			
		||||
                .frame(minHeight: 100)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								Workouts/Models/ExerciseList.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Workouts/Models/ExerciseList.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import Yams
 | 
			
		||||
 | 
			
		||||
struct ExerciseList: Codable {
 | 
			
		||||
    let name: String
 | 
			
		||||
    let source: String
 | 
			
		||||
    let exercises: [ExerciseItem]
 | 
			
		||||
    
 | 
			
		||||
    struct ExerciseItem: Codable, Identifiable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let descr: String
 | 
			
		||||
        let type: String
 | 
			
		||||
        
 | 
			
		||||
        var id: String { name }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ExerciseListLoader {
 | 
			
		||||
    static func loadExerciseLists() -> [String: ExerciseList] {
 | 
			
		||||
        var exerciseLists: [String: ExerciseList] = [:]
 | 
			
		||||
        
 | 
			
		||||
        guard let resourcePath = Bundle.main.resourcePath else {
 | 
			
		||||
            print("Could not find resource path")
 | 
			
		||||
            return exerciseLists
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            let fileManager = FileManager.default
 | 
			
		||||
            let resourceURL = URL(fileURLWithPath: resourcePath)
 | 
			
		||||
            let yamlFiles = try fileManager.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
 | 
			
		||||
                .filter { $0.pathExtension == "yaml" && $0.lastPathComponent.hasSuffix(".exercises.yaml") }
 | 
			
		||||
            
 | 
			
		||||
            for yamlFile in yamlFiles {
 | 
			
		||||
                let fileName = yamlFile.lastPathComponent
 | 
			
		||||
                do {
 | 
			
		||||
                    let yamlString = try String(contentsOf: yamlFile, encoding: .utf8)
 | 
			
		||||
                    if let exerciseList = try Yams.load(yaml: yamlString) as? [String: Any],
 | 
			
		||||
                       let name = exerciseList["name"] as? String,
 | 
			
		||||
                       let source = exerciseList["source"] as? String,
 | 
			
		||||
                       let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
 | 
			
		||||
                        
 | 
			
		||||
                        var exercises: [ExerciseList.ExerciseItem] = []
 | 
			
		||||
                        
 | 
			
		||||
                        for exerciseData in exercisesData {
 | 
			
		||||
                            if let name = exerciseData["name"] as? String,
 | 
			
		||||
                               let descr = exerciseData["descr"] as? String,
 | 
			
		||||
                               let type = exerciseData["type"] as? String {
 | 
			
		||||
                                let exercise = ExerciseList.ExerciseItem(name: name, descr: descr, type: type)
 | 
			
		||||
                                exercises.append(exercise)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        let exerciseList = ExerciseList(name: name, source: source, exercises: exercises)
 | 
			
		||||
                        exerciseLists[fileName] = exerciseList
 | 
			
		||||
                    }
 | 
			
		||||
                } catch {
 | 
			
		||||
                    print("Error loading YAML file \(fileName): \(error)")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("Error listing directory contents: \(error)")
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return exerciseLists
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
@Model
 | 
			
		||||
final class ExerciseType {
 | 
			
		||||
    var name: String = ""
 | 
			
		||||
    var descr: String = ""
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var exercises: [Exercise]? = []
 | 
			
		||||
    
 | 
			
		||||
    init(name: String, descr: String) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.descr = descr
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - EditableEntity Conformance
 | 
			
		||||
 | 
			
		||||
extension ExerciseType: EditableEntity {
 | 
			
		||||
    var count: Int? {
 | 
			
		||||
        return self.exercises?.count
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func createNew() -> ExerciseType {
 | 
			
		||||
        return ExerciseType(name: "", descr: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
        return "Exercise Types"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    static func formView(for model: ExerciseType) -> some View {
 | 
			
		||||
        EntityAddEditView(model: model) { $model in
 | 
			
		||||
            Section(header: Text("Name")) {
 | 
			
		||||
                TextField("Name", text: $model.name)
 | 
			
		||||
                    .bold()
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Section(header: Text("Description")) {
 | 
			
		||||
                TextEditor(text: $model.descr)
 | 
			
		||||
                    .frame(minHeight: 100)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
@Model
 | 
			
		||||
final class Muscle {
 | 
			
		||||
    var name: String = ""
 | 
			
		||||
    
 | 
			
		||||
    var descr: String = ""
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \MuscleGroup.muscles)
 | 
			
		||||
    var muscleGroup: MuscleGroup?
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var exercises: [Exercise]? = []
 | 
			
		||||
    
 | 
			
		||||
    init(name: String, descr: String, muscleGroup: MuscleGroup? = nil) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.descr = descr
 | 
			
		||||
        self.muscleGroup = muscleGroup
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - EditableEntity Conformance
 | 
			
		||||
 | 
			
		||||
extension Muscle: EditableEntity {
 | 
			
		||||
    var count: Int? {
 | 
			
		||||
        return self.exercises?.count
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func createNew() -> Muscle {
 | 
			
		||||
        return Muscle(name: "", descr: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
        return "Muscles"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    static func formView(for model: Muscle) -> some View {
 | 
			
		||||
        EntityAddEditView(model: model) { $model in
 | 
			
		||||
            MuscleFormView(model: $model)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - Private Form View
 | 
			
		||||
 | 
			
		||||
fileprivate struct MuscleFormView: View {
 | 
			
		||||
    @Binding var model: Muscle
 | 
			
		||||
    @Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Section(header: Text("Name")) {
 | 
			
		||||
            TextField("Name", text: $model.name)
 | 
			
		||||
                .bold()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Muscle Group")) {
 | 
			
		||||
            Picker("Muscle Group", selection: $model.muscleGroup) {
 | 
			
		||||
                Text("Select a muscle group").tag(nil as MuscleGroup?)
 | 
			
		||||
                ForEach(muscleGroups) { group in
 | 
			
		||||
                    Text(group.name).tag(group as MuscleGroup?)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Description")) {
 | 
			
		||||
            TextEditor(text: $model.descr)
 | 
			
		||||
                .frame(minHeight: 100)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
@Model
 | 
			
		||||
final class MuscleGroup {
 | 
			
		||||
    var name: String = ""
 | 
			
		||||
    var descr: String = ""
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var muscles: [Muscle]? = []
 | 
			
		||||
    
 | 
			
		||||
    init(name: String, descr: String) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.descr = descr
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - EditableEntity Conformance
 | 
			
		||||
 | 
			
		||||
extension MuscleGroup: EditableEntity {
 | 
			
		||||
    static func createNew() -> MuscleGroup {
 | 
			
		||||
        return MuscleGroup(name: "", descr: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
        return "Muscle Groups"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    static func formView(for model: MuscleGroup) -> some View {
 | 
			
		||||
        EntityAddEditView(model: model) { $model in
 | 
			
		||||
            Section(header: Text("Name")) {
 | 
			
		||||
                TextField("Name", text: $model.name)
 | 
			
		||||
                    .bold()
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Section(header: Text("Description")) {
 | 
			
		||||
                TextEditor(text: $model.descr)
 | 
			
		||||
                    .frame(minHeight: 100)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var count: Int? {
 | 
			
		||||
        return muscles?.count
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -5,27 +5,13 @@ import SwiftUI
 | 
			
		||||
@Model
 | 
			
		||||
final class Split {
 | 
			
		||||
    var name: String = ""
 | 
			
		||||
    var intro: String = ""
 | 
			
		||||
    var color: String = "indigo"
 | 
			
		||||
    var systemImage: String = "dumbbell.fill"
 | 
			
		||||
    var order: Int = 0
 | 
			
		||||
    
 | 
			
		||||
    // Returns the SwiftUI Color for the stored color name
 | 
			
		||||
    func getColor() -> Color {
 | 
			
		||||
        switch color {
 | 
			
		||||
        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
 | 
			
		||||
        }
 | 
			
		||||
    func getColor () -> Color {
 | 
			
		||||
        return Color.color(from: self.color)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
 | 
			
		||||
@@ -34,11 +20,11 @@ final class Split {
 | 
			
		||||
    @Relationship(deleteRule: .nullify, inverse: \Workout.split)
 | 
			
		||||
    var workouts: [Workout]? = []
 | 
			
		||||
    
 | 
			
		||||
    init(name: String, intro: String, color: String = "indigo", systemImage: String = "dumbbell.fill") {
 | 
			
		||||
    init(name: String, color: String = "indigo", systemImage: String = "dumbbell.fill", order: Int = 0) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.intro = intro
 | 
			
		||||
        self.color = color
 | 
			
		||||
        self.systemImage = systemImage
 | 
			
		||||
        self.order = order
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static let unnamed = "Unnamed Split"
 | 
			
		||||
@@ -52,7 +38,7 @@ extension Split: EditableEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func createNew() -> Split {
 | 
			
		||||
        return Split(name: "", intro: "")
 | 
			
		||||
        return Split(name: "")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static var navigationTitle: String {
 | 
			
		||||
@@ -72,10 +58,6 @@ extension Split: EditableEntity {
 | 
			
		||||
fileprivate struct SplitFormView: View {
 | 
			
		||||
    @Binding var model: Split
 | 
			
		||||
    
 | 
			
		||||
    @State private var showingAddSheet: Bool = false
 | 
			
		||||
    @State private var itemToEdit: SplitExerciseAssignment? = nil
 | 
			
		||||
    @State private var itemToDelete: SplitExerciseAssignment? = nil
 | 
			
		||||
    
 | 
			
		||||
    // Available colors for splits
 | 
			
		||||
    private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
 | 
			
		||||
    
 | 
			
		||||
@@ -88,15 +70,10 @@ fileprivate struct SplitFormView: View {
 | 
			
		||||
                .bold()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Description")) {
 | 
			
		||||
            TextEditor(text: $model.intro)
 | 
			
		||||
                .frame(minHeight: 100)
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Appearance")) {
 | 
			
		||||
            Picker("Color", selection: $model.color) {
 | 
			
		||||
                ForEach(availableColors, id: \.self) { colorName in
 | 
			
		||||
                    let tempSplit = Split(name: "", intro: "", color: colorName)
 | 
			
		||||
                    let tempSplit = Split(name: "", color: colorName)
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Circle()
 | 
			
		||||
                            .fill(tempSplit.getColor())
 | 
			
		||||
@@ -121,75 +98,7 @@ fileprivate struct SplitFormView: View {
 | 
			
		||||
        
 | 
			
		||||
        Section(header: Text("Exercises")) {
 | 
			
		||||
            NavigationLink {
 | 
			
		||||
                NavigationStack {
 | 
			
		||||
                    Form {
 | 
			
		||||
                        List {
 | 
			
		||||
                            if let assignments = model.exercises, !assignments.isEmpty {
 | 
			
		||||
                                let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exercise?.name ?? Exercise.unnamed < $1.exercise?.name ?? Exercise.unnamed : $0.order < $1.order })
 | 
			
		||||
                                ForEach(sortedAssignments) { item in
 | 
			
		||||
                                    ListItem(
 | 
			
		||||
                                        title: item.exercise?.name ?? Exercise.unnamed,
 | 
			
		||||
                                        text: item.setup.isEmpty ? nil : item.setup,
 | 
			
		||||
                                        subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .swipeActions {
 | 
			
		||||
                                        Button(role: .destructive) {
 | 
			
		||||
                                            itemToDelete = item
 | 
			
		||||
                                        } label: {
 | 
			
		||||
                                            Label("Delete", systemImage: "trash")
 | 
			
		||||
                                        }
 | 
			
		||||
                                        Button {
 | 
			
		||||
                                            itemToEdit = item
 | 
			
		||||
                                        } label: {
 | 
			
		||||
                                            Label("Edit", systemImage: "pencil")
 | 
			
		||||
                                        }
 | 
			
		||||
                                        .tint(.indigo)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Text("No exercises added yet.")
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .navigationTitle("Exercises")
 | 
			
		||||
                }
 | 
			
		||||
                .toolbar {
 | 
			
		||||
                    ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                        Button(action: { showingAddSheet.toggle() }) {
 | 
			
		||||
                            Image(systemName: "plus")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .sheet (isPresented: $showingAddSheet) {
 | 
			
		||||
                    ExercisePickerView { exercise in
 | 
			
		||||
                        itemToEdit = SplitExerciseAssignment(
 | 
			
		||||
                            order: 0,
 | 
			
		||||
                            sets: 3,
 | 
			
		||||
                            reps: 10,
 | 
			
		||||
                            weight: 40,
 | 
			
		||||
                            split: model,
 | 
			
		||||
                            exercise: exercise
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .sheet(item: $itemToEdit) { item in
 | 
			
		||||
                    SplitExerciseAssignmentAddEditView(model: item)
 | 
			
		||||
                }
 | 
			
		||||
                .confirmationDialog(
 | 
			
		||||
                    "Delete Exercise?",
 | 
			
		||||
                    isPresented: .constant(itemToDelete != nil),
 | 
			
		||||
                    titleVisibility: .visible
 | 
			
		||||
                ) {
 | 
			
		||||
                    Button("Delete", role: .destructive) {
 | 
			
		||||
                        if let item = itemToDelete {
 | 
			
		||||
                            withAnimation {
 | 
			
		||||
                                model.exercises?.removeAll { $0.id == item.id }
 | 
			
		||||
                                itemToDelete = nil
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                SplitExercisesListView(model: model)
 | 
			
		||||
            } label: {
 | 
			
		||||
                ListItem(
 | 
			
		||||
                    text: "Exercises",
 | 
			
		||||
 
 | 
			
		||||
@@ -3,25 +3,21 @@ import SwiftData
 | 
			
		||||
 | 
			
		||||
@Model
 | 
			
		||||
final class SplitExerciseAssignment {
 | 
			
		||||
    var exerciseName: String = ""
 | 
			
		||||
    var order: Int = 0
 | 
			
		||||
    var sets: Int = 0
 | 
			
		||||
    var reps: Int = 0
 | 
			
		||||
    var weight: Int = 0
 | 
			
		||||
    var setup: String = ""
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var split: Split?
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var exercise: Exercise?
 | 
			
		||||
    
 | 
			
		||||
    init(order: Int, sets: Int, reps: Int, weight: Int, setup: String = "", split: Split, exercise: Exercise) {
 | 
			
		||||
    init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
 | 
			
		||||
        self.split = split
 | 
			
		||||
        self.exerciseName = exerciseName
 | 
			
		||||
        self.order = order
 | 
			
		||||
        self.sets = sets
 | 
			
		||||
        self.reps = reps
 | 
			
		||||
        self.weight = weight
 | 
			
		||||
        self.setup = setup
 | 
			
		||||
        self.split = split
 | 
			
		||||
        self.exercise = exercise
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,16 +9,14 @@ final class WorkoutLog {
 | 
			
		||||
    var weight: Int = 0
 | 
			
		||||
    var status: WorkoutStatus? = WorkoutStatus.notStarted
 | 
			
		||||
    var order: Int = 0
 | 
			
		||||
    var exerciseName: String = ""
 | 
			
		||||
    
 | 
			
		||||
    var completed: Bool = false
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var workout: Workout?
 | 
			
		||||
    
 | 
			
		||||
    @Relationship(deleteRule: .nullify)
 | 
			
		||||
    var exercise: Exercise?
 | 
			
		||||
    
 | 
			
		||||
    init(workout: Workout, exercise: Exercise, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
 | 
			
		||||
    init(workout: Workout, exerciseName: String, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
 | 
			
		||||
        self.date = date
 | 
			
		||||
        self.order = order
 | 
			
		||||
        self.sets = sets
 | 
			
		||||
@@ -26,7 +24,7 @@ final class WorkoutLog {
 | 
			
		||||
        self.weight = weight
 | 
			
		||||
        self.status = status
 | 
			
		||||
        self.workout = workout
 | 
			
		||||
        self.exercise = exercise
 | 
			
		||||
        self.exerciseName = exerciseName
 | 
			
		||||
        self.completed = completed
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
name: Beginner
 | 
			
		||||
source: Planet Fitness
 | 
			
		||||
exercises:
 | 
			
		||||
- name: Lat Pull Down
 | 
			
		||||
  setup: 'Seat: 3, Thigh Pad: 4'
 | 
			
		||||
  descr: Sit upright with your knees secured under the pad. Grip the bar wider than
 | 
			
		||||
							
								
								
									
										135
									
								
								Workouts/Resources/_attic_/muscles.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								Workouts/Resources/_attic_/muscles.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
- name: Pectoralis Major
 | 
			
		||||
  muscleGroup: Chest
 | 
			
		||||
  descr: Large chest muscle located on the front of the upper torso, responsible for arm flexion and pushing.
 | 
			
		||||
- name: Pectoralis Minor
 | 
			
		||||
  muscleGroup: Chest
 | 
			
		||||
  descr: Thin, triangular muscle beneath the pectoralis major, stabilizes the scapula.
 | 
			
		||||
- name: Latissimus Dorsi
 | 
			
		||||
  muscleGroup: Back
 | 
			
		||||
  descr: Broad muscle on the mid to lower back, responsible for pulling and shoulder extension.
 | 
			
		||||
- name: Trapezius
 | 
			
		||||
  muscleGroup: Back
 | 
			
		||||
  descr: Large muscle covering the upper back and neck, involved in shoulder movement and posture.
 | 
			
		||||
- name: Rhomboids
 | 
			
		||||
  muscleGroup: Back
 | 
			
		||||
  descr: Muscles between the shoulder blades, responsible for scapular retraction.
 | 
			
		||||
- name: Erector Spinae
 | 
			
		||||
  muscleGroup: Back
 | 
			
		||||
  descr: Long vertical muscles along the spine that maintain posture and extend the back.
 | 
			
		||||
- name: Deltoid (Anterior)
 | 
			
		||||
  muscleGroup: Shoulders
 | 
			
		||||
  descr: Front portion of the shoulder muscle, raises the arm forward.
 | 
			
		||||
- name: Deltoid (Lateral)
 | 
			
		||||
  muscleGroup: Shoulders
 | 
			
		||||
  descr: Middle portion of the shoulder muscle, raises the arm to the side.
 | 
			
		||||
- name: Deltoid (Posterior)
 | 
			
		||||
  muscleGroup: Shoulders
 | 
			
		||||
  descr: Rear portion of the shoulder muscle, moves the arm backward.
 | 
			
		||||
- name: Rotator Cuff Muscles
 | 
			
		||||
  muscleGroup: Shoulders
 | 
			
		||||
  descr: Group of four muscles surrounding the shoulder joint, stabilizing and rotating the arm.
 | 
			
		||||
- name: Biceps Brachii
 | 
			
		||||
  muscleGroup: Arms
 | 
			
		||||
  descr: Front upper arm muscle, responsible for elbow flexion and forearm rotation.
 | 
			
		||||
- name: Triceps Brachii
 | 
			
		||||
  muscleGroup: Arms
 | 
			
		||||
  descr: Back upper arm muscle, responsible for elbow extension.
 | 
			
		||||
- name: Brachialis
 | 
			
		||||
  muscleGroup: Arms
 | 
			
		||||
  descr: Muscle beneath the biceps, assists in elbow flexion.
 | 
			
		||||
- name: Brachioradialis
 | 
			
		||||
  muscleGroup: Arms
 | 
			
		||||
  descr: Forearm muscle on the thumb side, aids in elbow flexion.
 | 
			
		||||
- name: Rectus Abdominis
 | 
			
		||||
  muscleGroup: Abdominals
 | 
			
		||||
  descr: Vertical muscle on the front of the abdomen, responsible for trunk flexion (the 'six-pack').
 | 
			
		||||
- name: Transverse Abdominis
 | 
			
		||||
  muscleGroup: Abdominals
 | 
			
		||||
  descr: Deepest abdominal muscle, wraps around the torso to stabilize the core.
 | 
			
		||||
- name: Internal Obliques
 | 
			
		||||
  muscleGroup: Abdominals
 | 
			
		||||
  descr: Muscles located beneath the external obliques, involved in trunk rotation and lateral flexion.
 | 
			
		||||
- name: External Obliques
 | 
			
		||||
  muscleGroup: Abdominals
 | 
			
		||||
  descr: Muscles on the sides of the abdomen, responsible for trunk twisting and side bending.
 | 
			
		||||
- name: Gluteus Maximus
 | 
			
		||||
  muscleGroup: Glutes
 | 
			
		||||
  descr: Largest glute muscle located in the buttocks, responsible for hip extension and rotation.
 | 
			
		||||
- name: Gluteus Medius
 | 
			
		||||
  muscleGroup: Glutes
 | 
			
		||||
  descr: Muscle on the outer surface of the pelvis, important for hip abduction and stability.
 | 
			
		||||
- name: Gluteus Minimus
 | 
			
		||||
  muscleGroup: Glutes
 | 
			
		||||
  descr: Smallest glute muscle, located beneath the medius, assists in hip abduction.
 | 
			
		||||
- name: Rectus Femoris
 | 
			
		||||
  muscleGroup: Quadriceps
 | 
			
		||||
  descr: Front thigh muscle, part of the quadriceps group, helps extend the knee and flex the hip.
 | 
			
		||||
- name: Vastus Lateralis
 | 
			
		||||
  muscleGroup: Quadriceps
 | 
			
		||||
  descr: Outer thigh muscle, part of the quadriceps, involved in knee extension.
 | 
			
		||||
- name: Vastus Medialis
 | 
			
		||||
  muscleGroup: Quadriceps
 | 
			
		||||
  descr: Inner thigh muscle, part of the quadriceps, assists in knee extension and stabilization.
 | 
			
		||||
- name: Vastus Intermedius
 | 
			
		||||
  muscleGroup: Quadriceps
 | 
			
		||||
  descr: Deep thigh muscle beneath rectus femoris, assists in knee extension.
 | 
			
		||||
- name: Biceps Femoris
 | 
			
		||||
  muscleGroup: Hamstrings
 | 
			
		||||
  descr: Muscle on the back of the thigh, responsible for knee flexion and hip extension.
 | 
			
		||||
- name: Semitendinosus
 | 
			
		||||
  muscleGroup: Hamstrings
 | 
			
		||||
  descr: Medial hamstring muscle, assists in knee flexion and internal rotation.
 | 
			
		||||
- name: Semimembranosus
 | 
			
		||||
  muscleGroup: Hamstrings
 | 
			
		||||
  descr: Deep medial hamstring muscle, also assists in knee flexion and hip extension.
 | 
			
		||||
- name: Gastrocnemius
 | 
			
		||||
  muscleGroup: Calves
 | 
			
		||||
  descr: Large calf muscle visible on the back of the lower leg, responsible for plantar flexion of the foot.
 | 
			
		||||
- name: Soleus
 | 
			
		||||
  muscleGroup: Calves
 | 
			
		||||
  descr: Flat muscle beneath the gastrocnemius, aids in plantar flexion when the knee is bent.
 | 
			
		||||
- name: Flexor Carpi Radialis
 | 
			
		||||
  muscleGroup: Forearms
 | 
			
		||||
  descr: Muscle on the front of the forearm, flexes and abducts the wrist.
 | 
			
		||||
- name: Flexor Carpi Ulnaris
 | 
			
		||||
  muscleGroup: Forearms
 | 
			
		||||
  descr: Forearm muscle that flexes and adducts the wrist.
 | 
			
		||||
- name: Extensor Carpi Radialis
 | 
			
		||||
  muscleGroup: Forearms
 | 
			
		||||
  descr: Posterior forearm muscle that extends and abducts the wrist.
 | 
			
		||||
- name: Pronator Teres
 | 
			
		||||
  muscleGroup: Forearms
 | 
			
		||||
  descr: Muscle running across the forearm that pronates the forearm (palm down).
 | 
			
		||||
- name: Sternocleidomastoid
 | 
			
		||||
  muscleGroup: Neck
 | 
			
		||||
  descr: Prominent neck muscle responsible for rotating and flexing the head.
 | 
			
		||||
- name: Splenius Capitis
 | 
			
		||||
  muscleGroup: Neck
 | 
			
		||||
  descr: Back of neck muscle that extends and rotates the head.
 | 
			
		||||
- name: Scalenes
 | 
			
		||||
  muscleGroup: Neck
 | 
			
		||||
  descr: Lateral neck muscles that assist in neck flexion and elevate the ribs during breathing.
 | 
			
		||||
- name: Iliopsoas
 | 
			
		||||
  muscleGroup: Hip Flexors
 | 
			
		||||
  descr: Deep muscle group connecting the lower spine to the femur, main hip flexor.
 | 
			
		||||
- name: Rectus Femoris
 | 
			
		||||
  muscleGroup: Hip Flexors
 | 
			
		||||
  descr: Also part of the quadriceps, helps flex the hip and extend the knee.
 | 
			
		||||
- name: Sartorius
 | 
			
		||||
  muscleGroup: Hip Flexors
 | 
			
		||||
  descr: Longest muscle in the body, crosses the thigh to flex and rotate the hip and knee.
 | 
			
		||||
- name: Adductor Longus
 | 
			
		||||
  muscleGroup: Adductors
 | 
			
		||||
  descr: Medial thigh muscle that adducts the leg and assists with hip flexion.
 | 
			
		||||
- name: Adductor Brevis
 | 
			
		||||
  muscleGroup: Adductors
 | 
			
		||||
  descr: Short adductor muscle that helps pull the thigh inward.
 | 
			
		||||
- name: Adductor Magnus
 | 
			
		||||
  muscleGroup: Adductors
 | 
			
		||||
  descr: Large, deep inner thigh muscle that performs hip adduction and extension.
 | 
			
		||||
- name: Gracilis
 | 
			
		||||
  muscleGroup: Adductors
 | 
			
		||||
  descr: Thin inner thigh muscle that assists in adduction and knee flexion.
 | 
			
		||||
- name: Tensor Fasciae Latae
 | 
			
		||||
  muscleGroup: Abductors
 | 
			
		||||
  descr: Lateral hip muscle that abducts and medially rotates the thigh.
 | 
			
		||||
							
								
								
									
										69
									
								
								Workouts/Resources/pf-starter.exercises.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								Workouts/Resources/pf-starter.exercises.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
name: Starter Set
 | 
			
		||||
source: Planet Fitness
 | 
			
		||||
exercises:
 | 
			
		||||
- name: Lat Pull Down
 | 
			
		||||
  descr: Sit upright with your knees secured under the pad. Grip the bar wider than
 | 
			
		||||
    shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades
 | 
			
		||||
    together. Avoid leaning back excessively or using momentum.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Seated Row
 | 
			
		||||
  descr: With your chest firmly against the pad, grip the handles and pull straight
 | 
			
		||||
    back while keeping your elbows close to your body. Focus on retracting your shoulder
 | 
			
		||||
    blades and avoid rounding your back.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Shoulder Press
 | 
			
		||||
  descr: Sit with your back against the pad, grip the handles just outside shoulder-width.
 | 
			
		||||
    Press upward without locking out your elbows. Keep your neck relaxed and avoid
 | 
			
		||||
    shrugging your shoulders.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Chest Press
 | 
			
		||||
  descr: Adjust the seat so the handles are at mid-chest height. Push forward until arms
 | 
			
		||||
    are nearly extended, then return slowly. Keep wrists straight and dont let your elbows
 | 
			
		||||
    drop too low.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Tricep Press
 | 
			
		||||
  descr: With elbows close to your sides, press the handles downward in a controlled
 | 
			
		||||
    motion. Avoid flaring your elbows or using your shoulders to assist the motion.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Arm Curl
 | 
			
		||||
  descr: Position your arms over the pad and grip the handles. Curl the weight upward
 | 
			
		||||
    while keeping your upper arms stationary. Avoid using momentum and fully control
 | 
			
		||||
    the lowering phase.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Abdominal
 | 
			
		||||
  descr: Sit with the pads resting against your chest. Contract your abs to curl forward,
 | 
			
		||||
    keeping your lower back in contact with the pad. Avoid pulling with your arms
 | 
			
		||||
    or hips.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Rotary
 | 
			
		||||
  descr: Rotate your torso from side to side in a controlled motion, keeping your
 | 
			
		||||
    hips still. Focus on using your obliques to generate the twist, not momentum or
 | 
			
		||||
    the arms.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Leg Press
 | 
			
		||||
  descr: Place your feet shoulder-width on the platform. Press upward through your
 | 
			
		||||
    heels without locking your knees. Keep your back flat against the pad throughout
 | 
			
		||||
    the motion.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Leg Extension
 | 
			
		||||
  descr: Sit upright and align your knees with the pivot point. Extend your legs to
 | 
			
		||||
    a straightened position, then lower with control. Avoid jerky movements or lifting
 | 
			
		||||
    your hips off the seat.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Leg Curl
 | 
			
		||||
  descr: Lie face down or sit depending on the version. Curl your legs toward your
 | 
			
		||||
    glutes, focusing on hamstring engagement. Avoid arching your back or using momentum.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Adductor
 | 
			
		||||
  descr: Sit with legs placed outside the pads. Bring your legs together using inner
 | 
			
		||||
    thigh muscles. Control the motion both in and out, avoiding fast swings.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Abductor
 | 
			
		||||
  descr: Sit with legs inside the pads and push outward to engage outer thighs and
 | 
			
		||||
    glutes. Avoid leaning forward and keep the motion controlled throughout.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
- name: Calfs
 | 
			
		||||
  descr: Place the balls of your feet on the platform with heels hanging off. Raise
 | 
			
		||||
    your heels by contracting your calves, then slowly lower them below the platform
 | 
			
		||||
    level for a full stretch.
 | 
			
		||||
  type: Machine-Based
 | 
			
		||||
@@ -1,167 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct DataLoader {
 | 
			
		||||
    static let logger = AppLogger(
 | 
			
		||||
        subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
 | 
			
		||||
        category: "InitialData"
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    // Data structures for JSON decoding
 | 
			
		||||
    private struct ExerciseTypeData: Codable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let descr: String
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private struct MuscleGroupData: Codable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let descr: String
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private struct MuscleData: Codable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let descr: String
 | 
			
		||||
        let muscleGroup: String
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private struct ExerciseData: Codable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let setup: String
 | 
			
		||||
        let descr: String
 | 
			
		||||
        let sets: Int
 | 
			
		||||
        let reps: Int
 | 
			
		||||
        let weight: Int
 | 
			
		||||
        let type: String
 | 
			
		||||
        let muscles: [String]
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private struct SplitExerciseAssignmentData: Codable {
 | 
			
		||||
        let exercise: String
 | 
			
		||||
        let weight: Int
 | 
			
		||||
        let sets: Int
 | 
			
		||||
        let reps: Int
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private struct SplitData: Codable {
 | 
			
		||||
        let name: String
 | 
			
		||||
        let intro: String
 | 
			
		||||
        let splitExerciseAssignments: [SplitExerciseAssignmentData]
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @MainActor
 | 
			
		||||
    static func create(modelContext: ModelContext) {
 | 
			
		||||
        logger.info("Creating initial data from JSON files")
 | 
			
		||||
        
 | 
			
		||||
        // Load and insert data
 | 
			
		||||
        do {
 | 
			
		||||
            // Dictionaries to store references
 | 
			
		||||
            var exerciseTypes: [String: ExerciseType] = [:]
 | 
			
		||||
            var muscleGroups: [String: MuscleGroup] = [:]
 | 
			
		||||
            var muscles: [String: Muscle] = [:]
 | 
			
		||||
            var exercises: [String: Exercise] = [:]
 | 
			
		||||
            
 | 
			
		||||
            // 1. Load Exercise Types
 | 
			
		||||
            let exerciseTypeData = try loadJSON(forResource: "exercise-types", type: [ExerciseTypeData].self)
 | 
			
		||||
            for typeData in exerciseTypeData {
 | 
			
		||||
                let exerciseType = ExerciseType(name: typeData.name, descr: typeData.descr)
 | 
			
		||||
                exerciseTypes[typeData.name] = exerciseType
 | 
			
		||||
                modelContext.insert(exerciseType)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // 2. Load Muscle Groups
 | 
			
		||||
            let muscleGroupData = try loadJSON(forResource: "muscle-groups", type: [MuscleGroupData].self)
 | 
			
		||||
            for groupData in muscleGroupData {
 | 
			
		||||
                let muscleGroup = MuscleGroup(name: groupData.name, descr: groupData.descr)
 | 
			
		||||
                muscleGroups[groupData.name] = muscleGroup
 | 
			
		||||
                modelContext.insert(muscleGroup)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // 3. Load Muscles
 | 
			
		||||
            let muscleData = try loadJSON(forResource: "muscles", type: [MuscleData].self)
 | 
			
		||||
            for data in muscleData {
 | 
			
		||||
                // Find the muscle group for this muscle
 | 
			
		||||
                if let muscleGroup = muscleGroups[data.muscleGroup] {
 | 
			
		||||
                    let muscle = Muscle(name: data.name, descr: data.descr, muscleGroup: muscleGroup)
 | 
			
		||||
                    muscles[data.name] = muscle
 | 
			
		||||
                    modelContext.insert(muscle)
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.warning("Muscle group not found for muscle: \(data.name)")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // 4. Load Exercises
 | 
			
		||||
            let exerciseData = try loadJSON(forResource: "exercises", type: [ExerciseData].self)
 | 
			
		||||
            for data in exerciseData {
 | 
			
		||||
                let exercise = Exercise(name: data.name, descr: data.descr)
 | 
			
		||||
                
 | 
			
		||||
                // Set exercise type
 | 
			
		||||
                if let type = exerciseTypes[data.type] {
 | 
			
		||||
                    exercise.type = type
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.warning("Exercise type not found: \(data.type) for exercise: \(data.name)")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Set muscles
 | 
			
		||||
                var exerciseMuscles: [Muscle] = []
 | 
			
		||||
                for muscleName in data.muscles {
 | 
			
		||||
                    if let muscle = muscles[muscleName] {
 | 
			
		||||
                        exerciseMuscles.append(muscle)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        logger.warning("Muscle not found: \(muscleName) for exercise: \(data.name)")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                exercise.muscles = exerciseMuscles
 | 
			
		||||
                
 | 
			
		||||
                exercises[data.name] = exercise
 | 
			
		||||
                modelContext.insert(exercise)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // 5. Load Splits and Exercise Assignments
 | 
			
		||||
            let splitData = try loadJSON(forResource: "splits", type: [SplitData].self)
 | 
			
		||||
            for data in splitData {
 | 
			
		||||
                let split = Split(name: data.name, intro: data.intro)
 | 
			
		||||
                modelContext.insert(split)
 | 
			
		||||
                
 | 
			
		||||
                // Create exercise assignments for this split
 | 
			
		||||
                for (index, assignment) in data.splitExerciseAssignments.enumerated() {
 | 
			
		||||
                    if let exercise = exercises[assignment.exercise] {
 | 
			
		||||
                        let splitAssignment = SplitExerciseAssignment(
 | 
			
		||||
                            order: index + 1,  // 1-based ordering
 | 
			
		||||
                            sets: assignment.sets,
 | 
			
		||||
                            reps: assignment.reps,
 | 
			
		||||
                            weight: assignment.weight,
 | 
			
		||||
                            split: split,
 | 
			
		||||
                            exercise: exercise
 | 
			
		||||
                        )
 | 
			
		||||
                        modelContext.insert(splitAssignment)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        logger.warning("Exercise not found: \(assignment.exercise) for split: \(data.name)")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Save all the inserted data
 | 
			
		||||
            try modelContext.save()
 | 
			
		||||
            logger.info("Initial data loaded successfully from JSON files")
 | 
			
		||||
        } catch {
 | 
			
		||||
            logger.error("Failed to load initial data from JSON files: \(error.localizedDescription)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Helper method to load and decode JSON from a file
 | 
			
		||||
    private static func loadJSON<T: Decodable>(forResource name: String, type: T.Type) throws -> T {
 | 
			
		||||
        guard let url = Bundle.main.url(forResource: name, withExtension: "json") else {
 | 
			
		||||
            logger.error("Could not find JSON file: \(name).json")
 | 
			
		||||
            throw NSError(domain: "InitialData", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find JSON file: \(name).json"])
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            let data = try Data(contentsOf: url)
 | 
			
		||||
            let decoder = JSONDecoder()
 | 
			
		||||
            return try decoder.decode(T.self, from: data)
 | 
			
		||||
        } catch {
 | 
			
		||||
            logger.error("Failed to decode JSON file \(name).json: \(error.localizedDescription)")
 | 
			
		||||
            throw error
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,10 +4,6 @@ enum SchemaV1: VersionedSchema {
 | 
			
		||||
    static var versionIdentifier: Schema.Version = .init(1, 0, 0)
 | 
			
		||||
    
 | 
			
		||||
    static var models: [any PersistentModel.Type] = [
 | 
			
		||||
        Exercise.self,
 | 
			
		||||
        ExerciseType.self,
 | 
			
		||||
        Muscle.self,
 | 
			
		||||
        MuscleGroup.self,
 | 
			
		||||
        Split.self,
 | 
			
		||||
        SplitExerciseAssignment.self,
 | 
			
		||||
        Workout.self,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,6 @@ enum SchemaV2: VersionedSchema {
 | 
			
		||||
    static var versionIdentifier: Schema.Version = .init(1, 0, 1)
 | 
			
		||||
    
 | 
			
		||||
    static var models: [any PersistentModel.Type] = [
 | 
			
		||||
        Exercise.self,
 | 
			
		||||
        ExerciseType.self,
 | 
			
		||||
        Muscle.self,
 | 
			
		||||
        MuscleGroup.self,
 | 
			
		||||
        Split.self,
 | 
			
		||||
        SplitExerciseAssignment.self,
 | 
			
		||||
        Workout.self,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,6 @@ enum SchemaV3: VersionedSchema {
 | 
			
		||||
    static var versionIdentifier: Schema.Version = .init(1, 0, 2)
 | 
			
		||||
    
 | 
			
		||||
    static var models: [any PersistentModel.Type] = [
 | 
			
		||||
        Exercise.self,
 | 
			
		||||
        ExerciseType.self,
 | 
			
		||||
        Muscle.self,
 | 
			
		||||
        MuscleGroup.self,
 | 
			
		||||
        Split.self,
 | 
			
		||||
        SplitExerciseAssignment.self,
 | 
			
		||||
        Workout.self,
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ final class WorkoutsContainer {
 | 
			
		||||
    
 | 
			
		||||
    static func create() -> ModelContainer {
 | 
			
		||||
        // Using the current models directly without migration plan to avoid reference errors
 | 
			
		||||
        let schema = Schema(SchemaV2.models)
 | 
			
		||||
        let schema = Schema(SchemaV1.models)
 | 
			
		||||
        let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
 | 
			
		||||
        let container = try! ModelContainer(for: schema, configurations: configuration)
 | 
			
		||||
        return container
 | 
			
		||||
@@ -20,13 +20,10 @@ final class WorkoutsContainer {
 | 
			
		||||
        let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            let schema = Schema(SchemaV2.models)
 | 
			
		||||
            let schema = Schema(SchemaV1.models)
 | 
			
		||||
            let container = try ModelContainer(for: schema, configurations: configuration)
 | 
			
		||||
            let context = ModelContext(container)
 | 
			
		||||
            
 | 
			
		||||
            // Create default data for previews
 | 
			
		||||
            DataLoader.create(modelContext: context)
 | 
			
		||||
            
 | 
			
		||||
            return container
 | 
			
		||||
        } catch {
 | 
			
		||||
            fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								Workouts/Utils/Color+color.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Workouts/Utils/Color+color.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
//
 | 
			
		||||
// Color+color.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/17/25 at 10:41 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUICore
 | 
			
		||||
 | 
			
		||||
extension Color {
 | 
			
		||||
    static func color (from: String) -> Color {
 | 
			
		||||
        switch from {
 | 
			
		||||
        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 .black
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,14 +18,9 @@ enum AppStorageKeys {
 | 
			
		||||
struct SettingsView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    
 | 
			
		||||
    @State private var showingPopulateData = false
 | 
			
		||||
    @State private var showingClearAllDataConfirmation = false
 | 
			
		||||
    
 | 
			
		||||
    var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
 | 
			
		||||
    var musclesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Muscle>()) }
 | 
			
		||||
    var muscleGroupsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<MuscleGroup>()) }
 | 
			
		||||
    var exerciseTypeCount: Int? { try? modelContext.fetchCount(FetchDescriptor<ExerciseType>()) }
 | 
			
		||||
    var exercisesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Exercise>()) }
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
@@ -40,67 +35,9 @@ struct SettingsView: View {
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    NavigationLink(destination: MuscleGroupsListView()) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text("Muscle Groups")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(muscleGroupsCount ?? 0)")
 | 
			
		||||
                                .font(.caption)
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    NavigationLink(destination: MusclesListView()) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text("Muscles")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(musclesCount ?? 0)")
 | 
			
		||||
                                .font(.caption)
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    NavigationLink(destination: ExerciseTypeListView()) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text("Exercise Types")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(exerciseTypeCount ?? 0)")
 | 
			
		||||
                                .font(.caption)
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    NavigationLink(destination: ExercisesListView()) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text("Exercises")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(exercisesCount ?? 0)")
 | 
			
		||||
                                .font(.caption)
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section(header: Text("Developer")) {
 | 
			
		||||
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingPopulateData = true
 | 
			
		||||
                    }) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Label("Populate Data", systemImage: "plus")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .confirmationDialog(
 | 
			
		||||
                        "Populate Data?",
 | 
			
		||||
                        isPresented: $showingPopulateData,
 | 
			
		||||
                        titleVisibility: .hidden
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Button("Populate Data") {
 | 
			
		||||
                            DataLoader.create(modelContext: modelContext)
 | 
			
		||||
                        }
 | 
			
		||||
                        Button("Cancel", role: .cancel) {}
 | 
			
		||||
//                    } message: {
 | 
			
		||||
//                        Text("This action cannot be undone. All data will be permanently deleted.")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingClearAllDataConfirmation = true
 | 
			
		||||
                    }) {
 | 
			
		||||
@@ -139,10 +76,6 @@ struct SettingsView: View {
 | 
			
		||||
 | 
			
		||||
    private func clearAllData () {
 | 
			
		||||
        do {
 | 
			
		||||
            try deleteAllObjects(ofType: ExerciseType.self, from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Exercise.self,     from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Muscle.self,       from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: MuscleGroup.self,  from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Split.self,        from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Workout.self,      from: modelContext)
 | 
			
		||||
@@ -154,30 +87,6 @@ struct SettingsView: View {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExercisesListView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        EntityListView<Exercise>(sort: [SortDescriptor(\Exercise.name)])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExerciseTypeListView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        EntityListView<ExerciseType>(sort: [SortDescriptor(\ExerciseType.name)])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct MuscleGroupsListView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        EntityListView<MuscleGroup>(sort: [SortDescriptor(\MuscleGroup.name)])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct MusclesListView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        EntityListView<Muscle>(sort: [SortDescriptor(\Muscle.name)])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct SplitsListView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,24 @@ import SwiftUI
 | 
			
		||||
struct SplitExerciseAssignmentAddEditView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    @State private var showingExercisePicker = false
 | 
			
		||||
 | 
			
		||||
    @State var model: SplitExerciseAssignment
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section (header: Text("Setup")) {
 | 
			
		||||
                    TextEditor(text: $model.setup)
 | 
			
		||||
                        .frame(minHeight: 60)
 | 
			
		||||
                Section(header: Text("Exercise")) {
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingExercisePicker = true
 | 
			
		||||
                    }) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text(model.exerciseName.isEmpty ? "Select Exercise" : model.exerciseName)
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Image(systemName: "chevron.right")
 | 
			
		||||
                                .foregroundColor(.gray)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section (header: Text("Sets/Reps")) {
 | 
			
		||||
@@ -44,7 +53,12 @@ struct SplitExerciseAssignmentAddEditView: View {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)")
 | 
			
		||||
            .sheet(isPresented: $showingExercisePicker) {
 | 
			
		||||
                ExercisePickerView { exerciseName in
 | 
			
		||||
                    model.exerciseName = exerciseName
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .navigationTitle(model.exerciseName.isEmpty ? "New Exercise" : model.exerciseName)
 | 
			
		||||
            .toolbar {
 | 
			
		||||
                ToolbarItem(placement: .navigationBarLeading) {
 | 
			
		||||
                    Button("Cancel") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								Workouts/Views/Splits/SplitAddEditView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Workouts/Views/Splits/SplitAddEditView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
//
 | 
			
		||||
// SplitAddEditView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/18/25 at 9:42 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct SplitAddEditView: View {
 | 
			
		||||
    @State var model: Split
 | 
			
		||||
    
 | 
			
		||||
    private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
 | 
			
		||||
    
 | 
			
		||||
    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 {
 | 
			
		||||
        Form {
 | 
			
		||||
            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 {
 | 
			
		||||
                    SplitExercisesListView(model: model)
 | 
			
		||||
                } label: {
 | 
			
		||||
                    ListItem(
 | 
			
		||||
                        text: "Exercises",
 | 
			
		||||
                        count: model.exercises?.count ?? 0
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								Workouts/Views/Splits/SplitExercisesListView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								Workouts/Views/Splits/SplitExercisesListView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
//
 | 
			
		||||
// SplitExercisesListView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/18/25 at 8:38 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct SplitExercisesListView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
 | 
			
		||||
    var model: Split
 | 
			
		||||
    
 | 
			
		||||
    @State private var showingAddSheet: Bool = false
 | 
			
		||||
    @State private var itemToEdit: SplitExerciseAssignment? = nil
 | 
			
		||||
    @State private var itemToDelete: SplitExerciseAssignment? = nil
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                List {
 | 
			
		||||
                    if let assignments = model.exercises, !assignments.isEmpty {
 | 
			
		||||
                        let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order })
 | 
			
		||||
                        
 | 
			
		||||
                        ForEach(sortedAssignments) { item in
 | 
			
		||||
                            ListItem(
 | 
			
		||||
                                title: item.exerciseName,
 | 
			
		||||
                                subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs  \(item.order)"
 | 
			
		||||
                            )
 | 
			
		||||
                            .swipeActions {
 | 
			
		||||
                                Button {
 | 
			
		||||
                                    itemToDelete = item
 | 
			
		||||
                                } label: {
 | 
			
		||||
                                    Label("Delete", systemImage: "circle")
 | 
			
		||||
                                }
 | 
			
		||||
                                Button {
 | 
			
		||||
                                    itemToEdit = item
 | 
			
		||||
                                } label: {
 | 
			
		||||
                                    Label("Edit", systemImage: "pencil")
 | 
			
		||||
                                }
 | 
			
		||||
                                .tint(.indigo)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        .onMove(perform: { indices, destination in
 | 
			
		||||
                            var exerciseArray = Array(sortedAssignments)
 | 
			
		||||
                            exerciseArray.move(fromOffsets: indices, toOffset: destination)
 | 
			
		||||
                            for (index, exercise) in exerciseArray.enumerated() {
 | 
			
		||||
                                exercise.order = index
 | 
			
		||||
                            }
 | 
			
		||||
                            if let modelContext = exerciseArray.first?.modelContext {
 | 
			
		||||
                                do {
 | 
			
		||||
                                    try modelContext.save()
 | 
			
		||||
                                } catch {
 | 
			
		||||
                                    print("Error saving after reordering: \(error)")
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Text("No exercises added yet.")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .navigationTitle("\(model.name)")
 | 
			
		||||
        }
 | 
			
		||||
        .toolbar {
 | 
			
		||||
            ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                Button(action: { showingAddSheet.toggle() }) {
 | 
			
		||||
                    Image(systemName: "plus")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .sheet (isPresented: $showingAddSheet) {
 | 
			
		||||
            ExercisePickerView { exerciseName in
 | 
			
		||||
                itemToEdit = SplitExerciseAssignment(
 | 
			
		||||
                    split: model,
 | 
			
		||||
                    exerciseName: exerciseName,
 | 
			
		||||
                    order: 0,
 | 
			
		||||
                    sets: 3,
 | 
			
		||||
                    reps: 10,
 | 
			
		||||
                    weight: 40
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .sheet(item: $itemToEdit) { item in
 | 
			
		||||
            SplitExerciseAssignmentAddEditView(model: item)
 | 
			
		||||
        }
 | 
			
		||||
        .confirmationDialog(
 | 
			
		||||
            "Delete Exercise?",
 | 
			
		||||
            isPresented: .constant(itemToDelete != nil),
 | 
			
		||||
            titleVisibility: .visible
 | 
			
		||||
        ) {
 | 
			
		||||
            Button("Delete", role: .destructive) {
 | 
			
		||||
                if let item = itemToDelete {
 | 
			
		||||
                    withAnimation {
 | 
			
		||||
                        modelContext.delete(item)
 | 
			
		||||
                        try? modelContext.save()
 | 
			
		||||
                        itemToDelete = nil
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								Workouts/Views/Splits/SplitsView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								Workouts/Views/Splits/SplitsView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
//
 | 
			
		||||
// SplitsView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/17/25 at 6:55 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct SplitsView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    
 | 
			
		||||
    @Query(sort: [
 | 
			
		||||
        SortDescriptor(\Split.order),
 | 
			
		||||
        SortDescriptor(\Split.name)
 | 
			
		||||
    ]) private var splits: [Split]
 | 
			
		||||
 | 
			
		||||
    @State private var showingAddSheet: Bool = false
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            ScrollView {
 | 
			
		||||
                LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
 | 
			
		||||
                    ForEach(splits) { split in
 | 
			
		||||
                        NavigationLink {
 | 
			
		||||
                            SplitExercisesListView(model: split)
 | 
			
		||||
                        } label: {
 | 
			
		||||
                            VStack {
 | 
			
		||||
                                ZStack(alignment: .bottom) {
 | 
			
		||||
                                    // Golden ratio rectangle (1:1.618)
 | 
			
		||||
                                    RoundedRectangle(cornerRadius: 12)
 | 
			
		||||
                                        .fill(
 | 
			
		||||
                                            LinearGradient(
 | 
			
		||||
                                                gradient: Gradient(colors: [split.getColor(), split.getColor().darker(by: 0.2)]),
 | 
			
		||||
                                                startPoint: .topLeading,
 | 
			
		||||
                                                endPoint: .bottomTrailing
 | 
			
		||||
                                            )
 | 
			
		||||
                                        )
 | 
			
		||||
                                        .aspectRatio(1.618, contentMode: .fit)
 | 
			
		||||
                                        .shadow(radius: 2)
 | 
			
		||||
                                    
 | 
			
		||||
                                    VStack {
 | 
			
		||||
                                        // Icon in the center
 | 
			
		||||
                                        Image(systemName: split.systemImage)
 | 
			
		||||
                                            .font(.system(size: 40, weight: .medium))
 | 
			
		||||
                                            .foregroundColor(.white)
 | 
			
		||||
                                            .offset(y: -15)
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Name at the bottom inside the rectangle
 | 
			
		||||
                                        Text(split.name)
 | 
			
		||||
                                            .font(.headline)
 | 
			
		||||
                                            .foregroundColor(.white)
 | 
			
		||||
                                            .lineLimit(1)
 | 
			
		||||
                                            .padding(.horizontal, 8)
 | 
			
		||||
                                            .padding(.bottom, 8)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                
 | 
			
		||||
                                // Exercise count below the rectangle
 | 
			
		||||
                                Text("\(split.exercises?.count ?? 0) exercises")
 | 
			
		||||
                                    .font(.caption)
 | 
			
		||||
                                    .foregroundColor(.secondary)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .onMove(perform: { indices, destination in
 | 
			
		||||
                        var splitArray = Array(splits)
 | 
			
		||||
                        splitArray.move(fromOffsets: indices, toOffset: destination)
 | 
			
		||||
                        for (index, split) in splitArray.enumerated() {
 | 
			
		||||
                            split.order = index
 | 
			
		||||
                        }
 | 
			
		||||
                        if let modelContext = splitArray.first?.modelContext {
 | 
			
		||||
                            do {
 | 
			
		||||
                                try modelContext.save()
 | 
			
		||||
                            } catch {
 | 
			
		||||
                                print("Error saving after reordering: \(error)")
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                .padding()
 | 
			
		||||
            }
 | 
			
		||||
            .navigationTitle("Splits")
 | 
			
		||||
        }
 | 
			
		||||
        .toolbar {
 | 
			
		||||
            ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                Button(action: { showingAddSheet.toggle() }) {
 | 
			
		||||
                    Image(systemName: "plus")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .sheet (isPresented: $showingAddSheet) {
 | 
			
		||||
            SplitAddEditView(model: Split(name: "New Split"))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								Workouts/Views/Workouts/CalendarListItem.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Workouts/Views/Workouts/CalendarListItem.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
//
 | 
			
		||||
// CalendarListItem.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/18/25 at 8:44 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct CalendarListItem: View {
 | 
			
		||||
    var date: Date
 | 
			
		||||
    var title: String
 | 
			
		||||
    var subtitle: String?
 | 
			
		||||
    var count: Int?
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack (alignment: .top) {
 | 
			
		||||
            ZStack {
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("\(date.abbreviatedWeekday)")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundColor(.secondary)
 | 
			
		||||
                    Text("\(date.dayOfMonth)")
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                        .foregroundColor(.accentColor)
 | 
			
		||||
                    Text("\(date.abbreviatedMonth)")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundColor(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
                .padding([.trailing], 10)
 | 
			
		||||
            }
 | 
			
		||||
            HStack {
 | 
			
		||||
                VStack (alignment: .leading) {
 | 
			
		||||
                    Text("\(title)")
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                    if let subtitle = subtitle {
 | 
			
		||||
                        Text("\(subtitle)")
 | 
			
		||||
                            .font(.footnote)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if let count = count {
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    Text("\(count)")
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundColor(.gray)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .frame(maxWidth: .infinity, alignment: .leading)
 | 
			
		||||
        .contentShape(Rectangle())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Date {
 | 
			
		||||
    private static let monthFormatter: DateFormatter = {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = Locale.current
 | 
			
		||||
        formatter.dateFormat = "MMM"
 | 
			
		||||
        return formatter
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    private static let dayFormatter: DateFormatter = {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = Locale.current
 | 
			
		||||
        formatter.dateFormat = "d"
 | 
			
		||||
        return formatter
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    private static let weekdayFormatter: DateFormatter = {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = Locale.current
 | 
			
		||||
        formatter.dateFormat = "E"
 | 
			
		||||
        return formatter
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    var abbreviatedMonth: String {
 | 
			
		||||
        Date.monthFormatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var dayOfMonth: String {
 | 
			
		||||
        Date.dayFormatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var abbreviatedWeekday: String {
 | 
			
		||||
        Date.weekdayFormatter.string(from: self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
//
 | 
			
		||||
// SplitPickerView.swift
 | 
			
		||||
// ExercisePickerView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/13/25 at 7:17 PM.
 | 
			
		||||
// Created by rzen on 7/13/25 at 7:17 PM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
@@ -11,37 +11,65 @@ import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct ExercisePickerView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
 | 
			
		||||
    @Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType]
 | 
			
		||||
    @State private var exerciseLists: [String: ExerciseList] = [:]  
 | 
			
		||||
    @State private var selectedListName: String? = nil
 | 
			
		||||
    
 | 
			
		||||
//    @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
 | 
			
		||||
    
 | 
			
		||||
    var onExerciseSelected: (Exercise) -> Void
 | 
			
		||||
    var onExerciseSelected: (String) -> Void
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            VStack {
 | 
			
		||||
                Form {
 | 
			
		||||
                    ForEach (exerciseTypes) { exerciseType in
 | 
			
		||||
                        if let exercises = exerciseType.exercises, !exercises.isEmpty {
 | 
			
		||||
                            let sortedExercises = exercises.sorted(by: { $0.name < $1.name })
 | 
			
		||||
                            Section (header: Text("\(exerciseType.name)")) {
 | 
			
		||||
                                List {
 | 
			
		||||
                                    ForEach(sortedExercises) { exercise in
 | 
			
		||||
                                        Button(action: {
 | 
			
		||||
                                            onExerciseSelected(exercise)
 | 
			
		||||
                                            dismiss()
 | 
			
		||||
                                        }) {
 | 
			
		||||
                                            ListItem(text: exercise.name)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        .buttonStyle(.plain)
 | 
			
		||||
            Group {
 | 
			
		||||
                if selectedListName == nil {
 | 
			
		||||
                    // Show list of exercise list files
 | 
			
		||||
                    List {
 | 
			
		||||
                        ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
 | 
			
		||||
                            if let list = exerciseLists[fileName] {
 | 
			
		||||
                                Button(action: {
 | 
			
		||||
                                    selectedListName = fileName
 | 
			
		||||
                                }) {
 | 
			
		||||
                                    VStack(alignment: .leading) {
 | 
			
		||||
                                        Text(list.name)
 | 
			
		||||
                                            .font(.headline)
 | 
			
		||||
                                        Text(list.source)
 | 
			
		||||
                                            .font(.subheadline)
 | 
			
		||||
                                            .foregroundColor(.secondary)
 | 
			
		||||
                                        Text("\(list.exercises.count) exercises")
 | 
			
		||||
                                            .font(.caption)
 | 
			
		||||
                                            .foregroundColor(.secondary)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.vertical, 4)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                    }
 | 
			
		||||
                    .navigationTitle("Exercise Lists")
 | 
			
		||||
                } else if let fileName = selectedListName, let list = exerciseLists[fileName] {
 | 
			
		||||
                    // Show exercises in the selected list
 | 
			
		||||
                    List {
 | 
			
		||||
                        ForEach(list.exercises) { exercise in
 | 
			
		||||
                            Button(action: {
 | 
			
		||||
                                onExerciseSelected(exercise.name)
 | 
			
		||||
                                dismiss()
 | 
			
		||||
                            }) {
 | 
			
		||||
                                VStack(alignment: .leading) {
 | 
			
		||||
                                    Text(exercise.name)
 | 
			
		||||
                                        .font(.headline)
 | 
			
		||||
                                    Text(exercise.type)
 | 
			
		||||
                                        .font(.caption)
 | 
			
		||||
                                        .foregroundColor(.secondary)
 | 
			
		||||
                                }
 | 
			
		||||
                                .padding(.vertical, 2)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .navigationTitle(list.name)
 | 
			
		||||
                    .toolbar {
 | 
			
		||||
                        ToolbarItem(placement: .navigationBarLeading) {
 | 
			
		||||
                            Button("Back") {
 | 
			
		||||
                                selectedListName = nil
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -53,5 +81,12 @@ struct ExercisePickerView: View {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            loadExerciseLists()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func loadExerciseLists() {
 | 
			
		||||
        exerciseLists = ExerciseListLoader.loadExerciseLists()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,8 @@ struct SplitPickerView: View {
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
 | 
			
		||||
    @Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
 | 
			
		||||
    @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
 | 
			
		||||
    
 | 
			
		||||
    var onSplitSelected: (Split) -> Void
 | 
			
		||||
//    var onExerciseSelected: (Exercise) -> Void
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ struct WorkoutEditView: View {
 | 
			
		||||
//                        List {
 | 
			
		||||
//                            ForEach (workoutLogs) { log in
 | 
			
		||||
//                                ListItem(
 | 
			
		||||
//                                    title: log.exercise?.name ?? Exercise.unnamed,
 | 
			
		||||
//                                    title: log.exerciseName,
 | 
			
		||||
//                                    subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
 | 
			
		||||
//                                )
 | 
			
		||||
//                            }
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ struct WorkoutLogEditView: View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section (header: Text("Exercise")) {
 | 
			
		||||
                    Text("\(workoutLog.exercise?.name ?? Exercise.unnamed)")
 | 
			
		||||
                    Text(workoutLog.exerciseName)
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
@@ -86,20 +86,20 @@ struct WorkoutLogEditView: View {
 | 
			
		||||
        let split = workoutLog.workout?.split
 | 
			
		||||
        
 | 
			
		||||
        // Find the matching exercise in split.exercises by name
 | 
			
		||||
//        if let exercises = split?.exercises {
 | 
			
		||||
//            for exerciseAssignment in exercises {
 | 
			
		||||
//                if exerciseAssignment.exercise.name == workoutLog.exercise.name {
 | 
			
		||||
//                    // Update the sets, reps, and weight in the split exercise assignment
 | 
			
		||||
//                    exerciseAssignment.sets = workoutLog.sets
 | 
			
		||||
//                    exerciseAssignment.reps = workoutLog.reps
 | 
			
		||||
//                    exerciseAssignment.weight = workoutLog.weight
 | 
			
		||||
//                    
 | 
			
		||||
//                    // Save the changes to the split
 | 
			
		||||
//                    try? modelContext.save()
 | 
			
		||||
//                    break
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
        if let exercises = split?.exercises {
 | 
			
		||||
            for exerciseAssignment in exercises {
 | 
			
		||||
                if exerciseAssignment.exerciseName == workoutLog.exerciseName {
 | 
			
		||||
                    // Update the sets, reps, and weight in the split exercise assignment
 | 
			
		||||
                    exerciseAssignment.sets = workoutLog.sets
 | 
			
		||||
                    exerciseAssignment.reps = workoutLog.reps
 | 
			
		||||
                    exerciseAssignment.weight = workoutLog.weight
 | 
			
		||||
                    
 | 
			
		||||
                    // Save the changes to the split
 | 
			
		||||
                    try? modelContext.save()
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ struct WorkoutLogView: View {
 | 
			
		||||
    var sortedWorkoutLogs: [WorkoutLog] {
 | 
			
		||||
        if let logs = workout.logs {
 | 
			
		||||
            logs.sorted(by: {
 | 
			
		||||
                $0.order == $1.order ? $0.exercise!.name < $1.exercise!.name : $0.order < $1.order
 | 
			
		||||
                $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            []
 | 
			
		||||
@@ -41,7 +41,7 @@ struct WorkoutLogView: View {
 | 
			
		||||
                        
 | 
			
		||||
                        CheckboxListItem(
 | 
			
		||||
                            status: workoutLogStatus,
 | 
			
		||||
                            title: log.exercise?.name ?? Exercise.unnamed,
 | 
			
		||||
                            title: log.exerciseName,
 | 
			
		||||
                            subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
 | 
			
		||||
                        )
 | 
			
		||||
                        
 | 
			
		||||
@@ -85,11 +85,12 @@ struct WorkoutLogView: View {
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
 | 
			
		||||
                            Button(role: .destructive) {
 | 
			
		||||
                            Button {
 | 
			
		||||
                                itemToDelete = log
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Label("Delete", systemImage: "trash")
 | 
			
		||||
                            }
 | 
			
		||||
                            .tint(.secondary)
 | 
			
		||||
                            Button {
 | 
			
		||||
                                itemToEdit = log
 | 
			
		||||
                            } label: {
 | 
			
		||||
@@ -110,11 +111,11 @@ struct WorkoutLogView: View {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .sheet(isPresented: $showingAddSheet) {
 | 
			
		||||
            ExercisePickerView { exercise in
 | 
			
		||||
                let setsRepsWeight = getSetsRepsWeight(exercise, in: modelContext)
 | 
			
		||||
            ExercisePickerView { exerciseName in
 | 
			
		||||
                let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext)
 | 
			
		||||
                let workoutLog = WorkoutLog(
 | 
			
		||||
                    workout: workout,
 | 
			
		||||
                    exercise: exercise,
 | 
			
		||||
                    exerciseName: exerciseName,
 | 
			
		||||
                    date: Date(),
 | 
			
		||||
                    sets: setsRepsWeight.sets,
 | 
			
		||||
                    reps: setsRepsWeight.reps,
 | 
			
		||||
@@ -149,20 +150,18 @@ struct WorkoutLogView: View {
 | 
			
		||||
                itemToDelete = nil
 | 
			
		||||
            }
 | 
			
		||||
        } message: {
 | 
			
		||||
            Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?")
 | 
			
		||||
            Text("Are you sure you want to delete workout started \(itemToDelete?.exerciseName ?? "this item")?")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func getSetsRepsWeight(_ exercise: Exercise, in modelContext: ModelContext) -> SetsRepsWeight {
 | 
			
		||||
    func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
 | 
			
		||||
        // Use a single expression predicate that works with SwiftData
 | 
			
		||||
        let exerciseID = exercise.persistentModelID
 | 
			
		||||
        
 | 
			
		||||
        print("Searching for exercise ID: \(exerciseID)")
 | 
			
		||||
        print("Searching for exercise name: \(exerciseName)")
 | 
			
		||||
        
 | 
			
		||||
        var descriptor = FetchDescriptor<WorkoutLog>(
 | 
			
		||||
            predicate: #Predicate<WorkoutLog> { log in
 | 
			
		||||
                log.exercise?.persistentModelID == exerciseID
 | 
			
		||||
                log.exerciseName == exerciseName
 | 
			
		||||
            },
 | 
			
		||||
            sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)]
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,8 @@ struct WorkoutsView: View {
 | 
			
		||||
                    List {
 | 
			
		||||
                        ForEach (workouts) { workout in
 | 
			
		||||
                            NavigationLink(destination: WorkoutLogView(workout: workout)) {
 | 
			
		||||
                                ListItem(
 | 
			
		||||
                                CalendarListItem(
 | 
			
		||||
                                    date: workout.start,
 | 
			
		||||
                                    title: workout.split?.name ?? Split.unnamed,
 | 
			
		||||
                                    subtitle: workout.label
 | 
			
		||||
                                )
 | 
			
		||||
@@ -95,23 +96,18 @@ struct WorkoutsView: View {
 | 
			
		||||
                SplitPickerView { split in
 | 
			
		||||
                    let workout = Workout(start: Date(), split: split)
 | 
			
		||||
                    modelContext.insert(workout)
 | 
			
		||||
                    
 | 
			
		||||
                    if let exercises = split.exercises {
 | 
			
		||||
                        for assignment in exercises {
 | 
			
		||||
                            if let exercise = assignment.exercise {
 | 
			
		||||
                                let workoutLog = WorkoutLog(
 | 
			
		||||
                                    workout: workout,
 | 
			
		||||
                                    exercise: exercise,
 | 
			
		||||
                                    date: Date(),
 | 
			
		||||
                                    order: assignment.order,
 | 
			
		||||
                                    sets: assignment.sets,
 | 
			
		||||
                                    reps: assignment.reps,
 | 
			
		||||
                                    weight: assignment.weight
 | 
			
		||||
                                )
 | 
			
		||||
                                modelContext.insert(workoutLog)
 | 
			
		||||
                            } else {
 | 
			
		||||
                                logger.debug("An exercise entity for a split is nil")
 | 
			
		||||
                            }
 | 
			
		||||
                            let workoutLog = WorkoutLog(
 | 
			
		||||
                                workout: workout,
 | 
			
		||||
                                exerciseName: assignment.exerciseName,
 | 
			
		||||
                                date: Date(),
 | 
			
		||||
                                order: assignment.order,
 | 
			
		||||
                                sets: assignment.sets,
 | 
			
		||||
                                reps: assignment.reps,
 | 
			
		||||
                                weight: assignment.weight
 | 
			
		||||
                            )
 | 
			
		||||
                            modelContext.insert(workoutLog)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    try? modelContext.save()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user