From 66f257609f29bda992190a2a66b2fd36139a16bf Mon Sep 17 00:00:00 2001 From: rzen Date: Fri, 18 Jul 2025 10:03:58 -0400 Subject: [PATCH] wip --- Workouts.xcodeproj/project.pbxproj | 25 +++ .../xcshareddata/swiftpm/Package.resolved | 15 ++ Workouts/ContentView.swift | 7 +- Workouts/Models/Exercise.swift | 72 -------- Workouts/Models/ExerciseList.swift | 66 +++++++ Workouts/Models/ExerciseType.swift | 48 ----- Workouts/Models/Muscle.swift | 73 -------- Workouts/Models/MuscleGroup.swift | 49 ----- Workouts/Models/Split.swift | 107 +---------- Workouts/Models/SplitExerciseAssignment.swift | 12 +- Workouts/Models/WorkoutLog.swift | 8 +- .../{ => _attic_}/exercise-types.json | 0 .../Resources/{ => _attic_}/exercises.json | 0 .../Resources/{ => _attic_}/exercises.yaml | 3 + .../{ => _attic_}/muscle-groups.json | 0 Workouts/Resources/{ => _attic_}/muscles.json | 0 Workouts/Resources/_attic_/muscles.yaml | 135 ++++++++++++++ Workouts/Resources/{ => _attic_}/splits.json | 0 Workouts/Resources/pf-starter.exercises.yaml | 69 ++++++++ Workouts/Schema/DataLoader.swift | 167 ------------------ Workouts/Schema/SchemaV1.swift | 4 - Workouts/Schema/SchemaV2.swift | 4 - Workouts/Schema/SchemaV3.swift | 4 - Workouts/Schema/WorkoutsContainer.swift | 7 +- Workouts/Utils/Color+color.swift | 30 ++++ Workouts/Views/Settings/SettingsView.swift | 91 ---------- .../SplitExerciseAssignmentAddEditView.swift | 22 ++- Workouts/Views/Splits/SplitAddEditView.swift | 64 +++++++ .../Views/Splits/SplitExercisesListView.swift | 108 +++++++++++ Workouts/Views/Splits/SplitsView.swift | 102 +++++++++++ .../Views/Workouts/CalendarListItem.swift | 89 ++++++++++ .../Views/Workouts/ExercisePickerView.swift | 83 ++++++--- Workouts/Views/Workouts/SplitPickerView.swift | 2 - Workouts/Views/Workouts/WorkoutEditView.swift | 2 +- .../Views/Workouts/WorkoutLogEditView.swift | 30 ++-- Workouts/Views/Workouts/WorkoutLogView.swift | 23 ++- Workouts/Views/Workouts/WorkoutsView.swift | 28 ++- 37 files changed, 845 insertions(+), 704 deletions(-) create mode 100644 Workouts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Workouts/Models/Exercise.swift create mode 100644 Workouts/Models/ExerciseList.swift delete mode 100644 Workouts/Models/ExerciseType.swift delete mode 100644 Workouts/Models/Muscle.swift delete mode 100644 Workouts/Models/MuscleGroup.swift rename Workouts/Resources/{ => _attic_}/exercise-types.json (100%) rename Workouts/Resources/{ => _attic_}/exercises.json (100%) rename Workouts/Resources/{ => _attic_}/exercises.yaml (99%) rename Workouts/Resources/{ => _attic_}/muscle-groups.json (100%) rename Workouts/Resources/{ => _attic_}/muscles.json (100%) create mode 100644 Workouts/Resources/_attic_/muscles.yaml rename Workouts/Resources/{ => _attic_}/splits.json (100%) create mode 100644 Workouts/Resources/pf-starter.exercises.yaml delete mode 100644 Workouts/Schema/DataLoader.swift create mode 100644 Workouts/Utils/Color+color.swift create mode 100644 Workouts/Views/Splits/SplitAddEditView.swift create mode 100644 Workouts/Views/Splits/SplitExercisesListView.swift create mode 100644 Workouts/Views/Splits/SplitsView.swift create mode 100644 Workouts/Views/Workouts/CalendarListItem.swift diff --git a/Workouts.xcodeproj/project.pbxproj b/Workouts.xcodeproj/project.pbxproj index 6d17388..d4b38dd 100644 --- a/Workouts.xcodeproj/project.pbxproj +++ b/Workouts.xcodeproj/project.pbxproj @@ -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 */; } diff --git a/Workouts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Workouts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..151787d --- /dev/null +++ b/Workouts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/Workouts/ContentView.swift b/Workouts/ContentView.swift index 07c2fb4..6d82d16 100644 --- a/Workouts/ContentView.swift +++ b/Workouts/ContentView.swift @@ -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") } diff --git a/Workouts/Models/Exercise.swift b/Workouts/Models/Exercise.swift deleted file mode 100644 index 0ddd46f..0000000 --- a/Workouts/Models/Exercise.swift +++ /dev/null @@ -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) - } - } -} diff --git a/Workouts/Models/ExerciseList.swift b/Workouts/Models/ExerciseList.swift new file mode 100644 index 0000000..686969b --- /dev/null +++ b/Workouts/Models/ExerciseList.swift @@ -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 + } +} diff --git a/Workouts/Models/ExerciseType.swift b/Workouts/Models/ExerciseType.swift deleted file mode 100644 index 2d0db46..0000000 --- a/Workouts/Models/ExerciseType.swift +++ /dev/null @@ -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) - } - } - } -} diff --git a/Workouts/Models/Muscle.swift b/Workouts/Models/Muscle.swift deleted file mode 100644 index 9c56de6..0000000 --- a/Workouts/Models/Muscle.swift +++ /dev/null @@ -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) - } - } -} diff --git a/Workouts/Models/MuscleGroup.swift b/Workouts/Models/MuscleGroup.swift deleted file mode 100644 index 3912f84..0000000 --- a/Workouts/Models/MuscleGroup.swift +++ /dev/null @@ -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 - } -} - diff --git a/Workouts/Models/Split.swift b/Workouts/Models/Split.swift index c844f57..c75961a 100644 --- a/Workouts/Models/Split.swift +++ b/Workouts/Models/Split.swift @@ -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", diff --git a/Workouts/Models/SplitExerciseAssignment.swift b/Workouts/Models/SplitExerciseAssignment.swift index 2da50a8..5be8de5 100644 --- a/Workouts/Models/SplitExerciseAssignment.swift +++ b/Workouts/Models/SplitExerciseAssignment.swift @@ -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 } } diff --git a/Workouts/Models/WorkoutLog.swift b/Workouts/Models/WorkoutLog.swift index ce052a6..120effd 100644 --- a/Workouts/Models/WorkoutLog.swift +++ b/Workouts/Models/WorkoutLog.swift @@ -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 } } diff --git a/Workouts/Resources/exercise-types.json b/Workouts/Resources/_attic_/exercise-types.json similarity index 100% rename from Workouts/Resources/exercise-types.json rename to Workouts/Resources/_attic_/exercise-types.json diff --git a/Workouts/Resources/exercises.json b/Workouts/Resources/_attic_/exercises.json similarity index 100% rename from Workouts/Resources/exercises.json rename to Workouts/Resources/_attic_/exercises.json diff --git a/Workouts/Resources/exercises.yaml b/Workouts/Resources/_attic_/exercises.yaml similarity index 99% rename from Workouts/Resources/exercises.yaml rename to Workouts/Resources/_attic_/exercises.yaml index 327fa6a..e8e2ec4 100644 --- a/Workouts/Resources/exercises.yaml +++ b/Workouts/Resources/_attic_/exercises.yaml @@ -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 diff --git a/Workouts/Resources/muscle-groups.json b/Workouts/Resources/_attic_/muscle-groups.json similarity index 100% rename from Workouts/Resources/muscle-groups.json rename to Workouts/Resources/_attic_/muscle-groups.json diff --git a/Workouts/Resources/muscles.json b/Workouts/Resources/_attic_/muscles.json similarity index 100% rename from Workouts/Resources/muscles.json rename to Workouts/Resources/_attic_/muscles.json diff --git a/Workouts/Resources/_attic_/muscles.yaml b/Workouts/Resources/_attic_/muscles.yaml new file mode 100644 index 0000000..cb8e772 --- /dev/null +++ b/Workouts/Resources/_attic_/muscles.yaml @@ -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. diff --git a/Workouts/Resources/splits.json b/Workouts/Resources/_attic_/splits.json similarity index 100% rename from Workouts/Resources/splits.json rename to Workouts/Resources/_attic_/splits.json diff --git a/Workouts/Resources/pf-starter.exercises.yaml b/Workouts/Resources/pf-starter.exercises.yaml new file mode 100644 index 0000000..54814c5 --- /dev/null +++ b/Workouts/Resources/pf-starter.exercises.yaml @@ -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 diff --git a/Workouts/Schema/DataLoader.swift b/Workouts/Schema/DataLoader.swift deleted file mode 100644 index edf62aa..0000000 --- a/Workouts/Schema/DataLoader.swift +++ /dev/null @@ -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(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 - } - } -} diff --git a/Workouts/Schema/SchemaV1.swift b/Workouts/Schema/SchemaV1.swift index 65595d2..f265ed6 100644 --- a/Workouts/Schema/SchemaV1.swift +++ b/Workouts/Schema/SchemaV1.swift @@ -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, diff --git a/Workouts/Schema/SchemaV2.swift b/Workouts/Schema/SchemaV2.swift index 18328be..f3d9642 100644 --- a/Workouts/Schema/SchemaV2.swift +++ b/Workouts/Schema/SchemaV2.swift @@ -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, diff --git a/Workouts/Schema/SchemaV3.swift b/Workouts/Schema/SchemaV3.swift index be47da2..076a542 100644 --- a/Workouts/Schema/SchemaV3.swift +++ b/Workouts/Schema/SchemaV3.swift @@ -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, diff --git a/Workouts/Schema/WorkoutsContainer.swift b/Workouts/Schema/WorkoutsContainer.swift index 266f60e..87df3d2 100644 --- a/Workouts/Schema/WorkoutsContainer.swift +++ b/Workouts/Schema/WorkoutsContainer.swift @@ -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)") diff --git a/Workouts/Utils/Color+color.swift b/Workouts/Utils/Color+color.swift new file mode 100644 index 0000000..fe64df5 --- /dev/null +++ b/Workouts/Utils/Color+color.swift @@ -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 + } + } +} diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/Views/Settings/SettingsView.swift index 783b3d5..f9fce6a 100644 --- a/Workouts/Views/Settings/SettingsView.swift +++ b/Workouts/Views/Settings/SettingsView.swift @@ -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()) } - var musclesCount: Int? { try? modelContext.fetchCount(FetchDescriptor()) } - var muscleGroupsCount: Int? { try? modelContext.fetchCount(FetchDescriptor()) } - var exerciseTypeCount: Int? { try? modelContext.fetchCount(FetchDescriptor()) } - var exercisesCount: Int? { try? modelContext.fetchCount(FetchDescriptor()) } 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(sort: [SortDescriptor(\Exercise.name)]) - } -} - -struct ExerciseTypeListView: View { - var body: some View { - EntityListView(sort: [SortDescriptor(\ExerciseType.name)]) - } -} - -struct MuscleGroupsListView: View { - var body: some View { - EntityListView(sort: [SortDescriptor(\MuscleGroup.name)]) - } -} - -struct MusclesListView: View { - var body: some View { - EntityListView(sort: [SortDescriptor(\Muscle.name)]) - } -} - struct SplitsListView: View { var body: some View { EntityListView(sort: [SortDescriptor(\Split.name)]) diff --git a/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift b/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift index 0695454..7bbe694 100644 --- a/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift +++ b/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift @@ -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") { diff --git a/Workouts/Views/Splits/SplitAddEditView.swift b/Workouts/Views/Splits/SplitAddEditView.swift new file mode 100644 index 0000000..d43d90e --- /dev/null +++ b/Workouts/Views/Splits/SplitAddEditView.swift @@ -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 + ) + } + } + } + } +} diff --git a/Workouts/Views/Splits/SplitExercisesListView.swift b/Workouts/Views/Splits/SplitExercisesListView.swift new file mode 100644 index 0000000..5e0b8f2 --- /dev/null +++ b/Workouts/Views/Splits/SplitExercisesListView.swift @@ -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 + } + } + } + } + + } +} diff --git a/Workouts/Views/Splits/SplitsView.swift b/Workouts/Views/Splits/SplitsView.swift new file mode 100644 index 0000000..79d1045 --- /dev/null +++ b/Workouts/Views/Splits/SplitsView.swift @@ -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")) + } + + } +} diff --git a/Workouts/Views/Workouts/CalendarListItem.swift b/Workouts/Views/Workouts/CalendarListItem.swift new file mode 100644 index 0000000..e9f3d55 --- /dev/null +++ b/Workouts/Views/Workouts/CalendarListItem.swift @@ -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) + } +} diff --git a/Workouts/Views/Workouts/ExercisePickerView.swift b/Workouts/Views/Workouts/ExercisePickerView.swift index 0f372d0..63abe8f 100644 --- a/Workouts/Views/Workouts/ExercisePickerView.swift +++ b/Workouts/Views/Workouts/ExercisePickerView.swift @@ -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() } } diff --git a/Workouts/Views/Workouts/SplitPickerView.swift b/Workouts/Views/Workouts/SplitPickerView.swift index c1fb0c8..348d6b5 100644 --- a/Workouts/Views/Workouts/SplitPickerView.swift +++ b/Workouts/Views/Workouts/SplitPickerView.swift @@ -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 { diff --git a/Workouts/Views/Workouts/WorkoutEditView.swift b/Workouts/Views/Workouts/WorkoutEditView.swift index 195e622..1e5b7c4 100644 --- a/Workouts/Views/Workouts/WorkoutEditView.swift +++ b/Workouts/Views/Workouts/WorkoutEditView.swift @@ -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" // ) // } diff --git a/Workouts/Views/Workouts/WorkoutLogEditView.swift b/Workouts/Views/Workouts/WorkoutLogEditView.swift index 2dcb092..ac8741e 100644 --- a/Workouts/Views/Workouts/WorkoutLogEditView.swift +++ b/Workouts/Views/Workouts/WorkoutLogEditView.swift @@ -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 + } + } + } } } diff --git a/Workouts/Views/Workouts/WorkoutLogView.swift b/Workouts/Views/Workouts/WorkoutLogView.swift index 36d3c41..d84ace4 100644 --- a/Workouts/Views/Workouts/WorkoutLogView.swift +++ b/Workouts/Views/Workouts/WorkoutLogView.swift @@ -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( predicate: #Predicate { log in - log.exercise?.persistentModelID == exerciseID + log.exerciseName == exerciseName }, sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)] ) diff --git a/Workouts/Views/Workouts/WorkoutsView.swift b/Workouts/Views/Workouts/WorkoutsView.swift index d2dec20..82d88a2 100644 --- a/Workouts/Views/Workouts/WorkoutsView.swift +++ b/Workouts/Views/Workouts/WorkoutsView.swift @@ -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()