From bdaa406876bfa84064e42b2cef231d66772cc04a Mon Sep 17 00:00:00 2001 From: rzen Date: Sun, 13 Jul 2025 21:54:09 -0400 Subject: [PATCH] wip --- Workouts/Cepo/EditableEntity.swift | 27 ++++ Workouts/Cepo/EntityAddEditView.swift | 48 ++++++ .../EntityListView.swift} | 71 +++++---- Workouts/Cepo/NavigationStackChecker.swift | 15 ++ Workouts/ContentView.swift | 2 +- Workouts/Models/Exercise.swift | 64 +++++++- Workouts/Models/ExerciseType.swift | 36 ++++- Workouts/Models/Muscle.swift | 58 ++++++- Workouts/Models/MuscleGroup.swift | 37 ++++- Workouts/Models/Split.swift | 82 +++++++++- Workouts/Models/Workout.swift | 4 + Workouts/Models/WorkoutLog.swift | 2 +- Workouts/Schema/InitialData.swift | 5 +- Workouts/Schema/WorkoutsContainer.swift | 5 +- Workouts/Utils/AppLogger.swift | 21 +++ Workouts/Utils/ListItem.swift | 13 +- .../ExerciseTypeAddEditView.swift | 48 ------ .../ExerciseTypes/ExerciseTypeListView.swift | 75 --------- .../Exercises/ExerciseAddEditView.swift | 96 ----------- .../Exercises/ExercisesListView.swift | 95 ----------- .../MuscleGroups/MuscleGroupAddEditView.swift | 55 ------- .../Settings/Muscles/MuscleAddEditView.swift | 59 ------- .../Settings/Muscles/MusclesListView.swift | 79 --------- Workouts/Views/Settings/SettingsView.swift | 30 ++++ .../Settings/Splits/SplitAddEditView.swift | 78 --------- .../Settings/Splits/SplitsListView.swift | 78 --------- .../Views/Workouts/ExercisePickerView.swift | 49 ++++++ Workouts/Views/Workouts/SplitPickerView.swift | 73 +++++++++ .../Views/Workouts/WorkoutLogEditView.swift | 103 ++++++++++++ Workouts/Views/Workouts/WorkoutLogView.swift | 125 +++++++++++++++ Workouts/Views/Workouts/WorkoutView.swift | 11 ++ Workouts/Views/Workouts/WorkoutsView.swift | 150 ++++++++++++++++++ Workouts/Workouts.entitlements | 4 +- 33 files changed, 984 insertions(+), 714 deletions(-) create mode 100644 Workouts/Cepo/EditableEntity.swift create mode 100644 Workouts/Cepo/EntityAddEditView.swift rename Workouts/{Views/Settings/MuscleGroups/MuscleGroupsListView.swift => Cepo/EntityListView.swift} (51%) create mode 100644 Workouts/Cepo/NavigationStackChecker.swift delete mode 100644 Workouts/Views/Settings/ExerciseTypes/ExerciseTypeAddEditView.swift delete mode 100644 Workouts/Views/Settings/ExerciseTypes/ExerciseTypeListView.swift delete mode 100644 Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift delete mode 100644 Workouts/Views/Settings/Exercises/ExercisesListView.swift delete mode 100644 Workouts/Views/Settings/MuscleGroups/MuscleGroupAddEditView.swift delete mode 100644 Workouts/Views/Settings/Muscles/MuscleAddEditView.swift delete mode 100644 Workouts/Views/Settings/Muscles/MusclesListView.swift delete mode 100644 Workouts/Views/Settings/Splits/SplitAddEditView.swift delete mode 100644 Workouts/Views/Settings/Splits/SplitsListView.swift create mode 100644 Workouts/Views/Workouts/ExercisePickerView.swift create mode 100644 Workouts/Views/Workouts/SplitPickerView.swift create mode 100644 Workouts/Views/Workouts/WorkoutLogEditView.swift create mode 100644 Workouts/Views/Workouts/WorkoutLogView.swift create mode 100644 Workouts/Views/Workouts/WorkoutView.swift create mode 100644 Workouts/Views/Workouts/WorkoutsView.swift diff --git a/Workouts/Cepo/EditableEntity.swift b/Workouts/Cepo/EditableEntity.swift new file mode 100644 index 0000000..ea3f9b5 --- /dev/null +++ b/Workouts/Cepo/EditableEntity.swift @@ -0,0 +1,27 @@ +import SwiftUI +import SwiftData + +/// A protocol for entities that can be managed in a generic list view. +protocol EditableEntity: PersistentModel, Identifiable, Hashable { + /// The name of the entity to be displayed in the list. + var name: String { get set } + + /// A view for adding or editing the entity. + associatedtype FormView: View + @ViewBuilder static func formView(for model: Self) -> FormView + + /// Creates a new, empty instance of the entity. + static func createNew() -> Self + + /// The title for the navigation bar in the list view. + static var navigationTitle: String { get } + + /// An optional property to specify a count to be displayed in the list item. + var count: Int? { get } +} + +extension EditableEntity { + var count: Int? { + return nil + } +} diff --git a/Workouts/Cepo/EntityAddEditView.swift b/Workouts/Cepo/EntityAddEditView.swift new file mode 100644 index 0000000..b33bc34 --- /dev/null +++ b/Workouts/Cepo/EntityAddEditView.swift @@ -0,0 +1,48 @@ +import SwiftUI +import SwiftData + +struct EntityAddEditView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + + @State var model: T + private let content: (Binding) -> Content + + init(model: T, @ViewBuilder content: @escaping (Binding) -> Content) { + _model = State(initialValue: model) + self.content = content + } + + var body: some View { + NavigationStack { + Form { + content($model) + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + // If the model is not in a context, it's a new one. + if model.modelContext == nil { + modelContext.insert(model) + } + // The save is in a do-catch block to handle potential errors, + // such as constraint violations. + do { + try modelContext.save() + dismiss() + } catch { + // In a real app, you'd want to present this error to the user. + print("Failed to save model: \(error.localizedDescription)") + } + } + } + } + } + } +} diff --git a/Workouts/Views/Settings/MuscleGroups/MuscleGroupsListView.swift b/Workouts/Cepo/EntityListView.swift similarity index 51% rename from Workouts/Views/Settings/MuscleGroups/MuscleGroupsListView.swift rename to Workouts/Cepo/EntityListView.swift index 94ddb96..4b0a814 100644 --- a/Workouts/Views/Settings/MuscleGroups/MuscleGroupsListView.swift +++ b/Workouts/Cepo/EntityListView.swift @@ -1,31 +1,37 @@ -// -// MuscleGroupsListView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 12:14 PM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - import SwiftUI import SwiftData -struct MuscleGroupsListView: View { +struct EntityListView: View { @Environment(\.modelContext) private var modelContext - @Query(sort: [SortDescriptor(\MuscleGroup.name)]) var items: [MuscleGroup] + @Query var items: [T] - @State var showingAddSheet = false - @State var itemToEdit: MuscleGroup? = nil - @State var itemToDelete: MuscleGroup? = nil + @State private var showingAddSheet = false + @State private var itemToEdit: T? = nil + @State private var itemToDelete: T? = nil + private var sortDescriptors: [SortDescriptor] + + init(sort: [SortDescriptor] = [], searchString: String = "") { + self.sortDescriptors = sort + _items = Query(filter: #Predicate { item in + if searchString.isEmpty { + return true + } else { + return item.name.localizedStandardContains(searchString) + } + }, sort: sort) + } + + @State private var isInsideNavigationStack: Bool = false + var body: some View { - Form { + let content = Form { List { - ForEach (items) { item in - ListItem(title: item.name, count: item.muscles?.count) + ForEach(items) { item in + ListItem(title: item.name, count: item.count) .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button (role: .destructive) { + Button(role: .destructive) { itemToDelete = item } label: { Label("Delete", systemImage: "trash") @@ -40,21 +46,19 @@ struct MuscleGroupsListView: View { } } } - .navigationTitle("Muscle Groups") + .navigationTitle(T.navigationTitle) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - showingAddSheet.toggle() - }) { + Button(action: { showingAddSheet.toggle() }) { Image(systemName: "plus") } } } .sheet(isPresented: $showingAddSheet) { - MuscleGroupAddEditView() + T.formView(for: T.createNew()) } - .sheet(item: $itemToEdit) {item in - MuscleGroupAddEditView(model: item) + .sheet(item: $itemToEdit) { item in + T.formView(for: item) } .confirmationDialog( "Delete?", @@ -65,10 +69,10 @@ struct MuscleGroupsListView: View { titleVisibility: .visible ) { Button("Delete", role: .destructive) { - if let itemToDelete = itemToDelete { - modelContext.delete(itemToDelete) + if let item = itemToDelete { + modelContext.delete(item) try? modelContext.save() - self.itemToDelete = nil + itemToDelete = nil } } Button("Cancel", role: .cancel) { @@ -77,5 +81,16 @@ struct MuscleGroupsListView: View { } message: { Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?") } + .background( + NavigationStackChecker(isInside: $isInsideNavigationStack) + ) + + if isInsideNavigationStack { + content + } else { + NavigationStack { + content + } + } } } diff --git a/Workouts/Cepo/NavigationStackChecker.swift b/Workouts/Cepo/NavigationStackChecker.swift new file mode 100644 index 0000000..fba8658 --- /dev/null +++ b/Workouts/Cepo/NavigationStackChecker.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct NavigationStackChecker: UIViewControllerRepresentable { + @Binding var isInside: Bool + + func makeUIViewController(context: Context) -> UIViewController { + let viewController = UIViewController() + DispatchQueue.main.async { + self.isInside = viewController.navigationController != nil + } + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/Workouts/ContentView.swift b/Workouts/ContentView.swift index ab72fe7..6bcfb79 100644 --- a/Workouts/ContentView.swift +++ b/Workouts/ContentView.swift @@ -16,7 +16,7 @@ struct ContentView: View { var body: some View { TabView { - Text("Placeholder") + WorkoutsView() .tabItem { Label("Workout", systemImage: "figure.strengthtraining.traditional") } diff --git a/Workouts/Models/Exercise.swift b/Workouts/Models/Exercise.swift index a0c7cf6..3cdb608 100644 --- a/Workouts/Models/Exercise.swift +++ b/Workouts/Models/Exercise.swift @@ -1,9 +1,10 @@ import Foundation import SwiftData +import SwiftUI @Model -final class Exercise: ListableItem { - @Attribute(.unique) var name: String = "" +final class Exercise { + var name: String = "" var setup: String = "" var descr: String = "" var sets: Int = 0 @@ -31,3 +32,62 @@ final class Exercise: ListableItem { self.weight = weight } } + +extension Exercise: EditableEntity { + static func createNew() -> Exercise { + return Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30) + } + + 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) + } + + Section(header: Text("Setup")) { + TextEditor(text: $model.setup) + .frame(minHeight: 100) + } + + Section(header: Text("Weight")) { + HStack { + Text("\(model.weight)") + .bold() + Text("lbs") + Spacer() + Stepper("", value: $model.weight, in: 0...1000) + } + } + } +} diff --git a/Workouts/Models/ExerciseType.swift b/Workouts/Models/ExerciseType.swift index 5f55229..2d0db46 100644 --- a/Workouts/Models/ExerciseType.swift +++ b/Workouts/Models/ExerciseType.swift @@ -1,9 +1,10 @@ import Foundation import SwiftData +import SwiftUI @Model -final class ExerciseType: ListableItem { - @Attribute(.unique) var name: String = "" +final class ExerciseType { + var name: String = "" var descr: String = "" @Relationship(deleteRule: .nullify) @@ -14,3 +15,34 @@ final class ExerciseType: ListableItem { 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 index 9608857..9c56de6 100644 --- a/Workouts/Models/Muscle.swift +++ b/Workouts/Models/Muscle.swift @@ -1,9 +1,10 @@ import Foundation import SwiftData +import SwiftUI @Model -final class Muscle: ListableItem { - @Attribute(.unique) var name: String = "" +final class Muscle { + var name: String = "" var descr: String = "" @@ -13,9 +14,60 @@ final class Muscle: ListableItem { @Relationship(deleteRule: .nullify) var exercises: [Exercise]? = [] - init(name: String, descr: String, muscleGroup: MuscleGroup) { + 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 index b5c8e7e..3912f84 100644 --- a/Workouts/Models/MuscleGroup.swift +++ b/Workouts/Models/MuscleGroup.swift @@ -1,9 +1,10 @@ import Foundation import SwiftData +import SwiftUI @Model -final class MuscleGroup: ListableItem { - @Attribute(.unique) var name: String = "" +final class MuscleGroup { + var name: String = "" var descr: String = "" @Relationship(deleteRule: .nullify) @@ -14,3 +15,35 @@ final class MuscleGroup: ListableItem { 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 ae854a5..a0179db 100644 --- a/Workouts/Models/Split.swift +++ b/Workouts/Models/Split.swift @@ -1,9 +1,10 @@ import Foundation import SwiftData +import SwiftUI @Model -final class Split: ListableItem { - @Attribute(.unique) var name: String = "" +final class Split { + var name: String = "" var intro: String = "" @Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split) @@ -17,3 +18,80 @@ final class Split: ListableItem { self.intro = intro } } + +// MARK: - EditableEntity Conformance + +extension Split: EditableEntity { + var count: Int? { + return self.exercises?.count + } + + static func createNew() -> Split { + return Split(name: "", intro: "") + } + + static var navigationTitle: String { + return "Splits" + } + + @ViewBuilder + static func formView(for model: Split) -> some View { + EntityAddEditView(model: model) { $model in + SplitFormView(model: $model) + } + } +} + +// MARK: - Private Form View + +fileprivate struct SplitFormView: View { + @Binding var model: Split + @State private var itemToDelete: SplitExerciseAssignment? = nil + + var body: some View { + Section(header: Text("Name")) { + TextField("Name", text: $model.name) + .bold() + } + + Section(header: Text("Description")) { + TextEditor(text: $model.intro) + .frame(minHeight: 100) + } + + Section(header: Text("Exercises")) { + if let assignments = model.exercises, !assignments.isEmpty { + ForEach(assignments) { item in + ListItem( + title: item.exercise?.name ?? "Unnamed", + subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs" + ) + .swipeActions { + Button(role: .destructive) { + itemToDelete = item + } label: { + Label("Delete", systemImage: "trash") + } + } + } + } else { + Text("No exercises added yet.") + } + Button(action: { + // TODO: Implement add exercise functionality + }) { + Label("Add Exercise", systemImage: "plus.circle") + } + } + .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 + } + } + } + } + } +} diff --git a/Workouts/Models/Workout.swift b/Workouts/Models/Workout.swift index 5eed9ed..ad7fc35 100644 --- a/Workouts/Models/Workout.swift +++ b/Workouts/Models/Workout.swift @@ -17,4 +17,8 @@ final class Workout { self.end = end self.split = split } + + var label: String { + start.formattedDate() + } } diff --git a/Workouts/Models/WorkoutLog.swift b/Workouts/Models/WorkoutLog.swift index 29846d2..e09fa3a 100644 --- a/Workouts/Models/WorkoutLog.swift +++ b/Workouts/Models/WorkoutLog.swift @@ -15,7 +15,7 @@ final class WorkoutLog { @Relationship(deleteRule: .nullify) var exercise: Exercise? - init(date: Date, sets: Int, reps: Int, weight: Int, completed: Bool, workout: Workout, exercise: Exercise) { + init(workout: Workout, exercise: Exercise, date: Date, sets: Int, reps: Int, weight: Int, completed: Bool) { self.date = date self.sets = sets self.reps = reps diff --git a/Workouts/Schema/InitialData.swift b/Workouts/Schema/InitialData.swift index 0273517..cce3901 100644 --- a/Workouts/Schema/InitialData.swift +++ b/Workouts/Schema/InitialData.swift @@ -2,7 +2,10 @@ import Foundation import SwiftData struct InitialData { - static let logger = AppLogger(subsystem: "Workouts", category: "InitialData") + static let logger = AppLogger( + subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", + category: "InitialData" + ) // Data structures for JSON decoding private struct ExerciseTypeData: Codable { diff --git a/Workouts/Schema/WorkoutsContainer.swift b/Workouts/Schema/WorkoutsContainer.swift index 3be23a3..426b36c 100644 --- a/Workouts/Schema/WorkoutsContainer.swift +++ b/Workouts/Schema/WorkoutsContainer.swift @@ -6,10 +6,11 @@ final class WorkoutsContainer { static func create(shouldCreateDefaults: inout Bool) -> ModelContainer { let schema = Schema(versionedSchema: SchemaV1.self) - let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self) + let configuration = ModelConfiguration(cloudKitDatabase: .automatic) + let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self, configurations: [configuration]) let context = ModelContext(container) - let descriptor = FetchDescriptor() + let descriptor = FetchDescriptor() let results = try! context.fetch(descriptor) if results.isEmpty { diff --git a/Workouts/Utils/AppLogger.swift b/Workouts/Utils/AppLogger.swift index 3ab1bea..3f31bad 100644 --- a/Workouts/Utils/AppLogger.swift +++ b/Workouts/Utils/AppLogger.swift @@ -1,4 +1,5 @@ import OSLog +import SwiftUI struct AppLogger { private let logger: Logger @@ -34,4 +35,24 @@ struct AppLogger { func error(_ message: String) { logger.error("\(formattedMessage(message))") } + + func vdebug(_ message: String) -> any View { + logger.debug("\(formattedMessage(message))") + return EmptyView() + } + + func vinfo(_ message: String) -> any View { + logger.info("\(formattedMessage(message))") + return EmptyView() + } + + func vwarning(_ message: String) -> any View { + logger.warning("\(formattedMessage(message))") + return EmptyView() + } + + func verror(_ message: String) -> any View { + logger.error("\(formattedMessage(message))") + return EmptyView() + } } diff --git a/Workouts/Utils/ListItem.swift b/Workouts/Utils/ListItem.swift index def4589..e2bbceb 100644 --- a/Workouts/Utils/ListItem.swift +++ b/Workouts/Utils/ListItem.swift @@ -19,21 +19,22 @@ struct ListItem: View { HStack { VStack (alignment: .leading) { Text("\(title)") - HStack { - if let subtitle = subtitle { - Text("\(subtitle)") - .font(.footnote) - } + .font(.headline) + HStack (alignment: .bottom) { if let badges = badges { ForEach (badges, id: \.self) { badge in Text("\(badge.text)") .bold() .padding([.leading,.trailing], 5) - .cornerRadius(4) .background(badge.color) .foregroundColor(.white) + .cornerRadius(4) } } + if let subtitle = subtitle { + Text("\(subtitle)") + .font(.footnote) + } } } if let count = count { diff --git a/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeAddEditView.swift b/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeAddEditView.swift deleted file mode 100644 index ca9e4cd..0000000 --- a/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeAddEditView.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ExerciseTypeAddEditView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 11:33 AM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI - -struct ExerciseTypeAddEditView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Bindable var model: ExerciseType - - var body: some View { - NavigationStack { - Form { - Section (header: Text("Nname")) { - TextField("Name", text: $model.name) - .bold() - } - - Section(header: Text("Description")) { - TextEditor(text: $model.descr) - .frame(minHeight: 100) - .padding(.vertical, 4) - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - try? modelContext.save() - dismiss() - } - } - } - } - } -} diff --git a/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeListView.swift b/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeListView.swift deleted file mode 100644 index e4d78d9..0000000 --- a/Workouts/Views/Settings/ExerciseTypes/ExerciseTypeListView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// ExerciseTypeListView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 11:27 AM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI -import SwiftData - -struct ExerciseTypeListView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\ExerciseType.name)]) var items: [ExerciseType] - - @State var itemToEdit: ExerciseType? = nil - @State var itemToDelete: ExerciseType? = nil - - private func save () { - try? modelContext.save() - } - - var body: some View { - NavigationStack { - Form { - List { - ForEach (items) { item in - ListItem(title: item.name) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button (role: .destructive) { - itemToDelete = item - } label: { - Label("Delete", systemImage: "trash") - } - Button { - itemToEdit = item - } label: { - Label("Edit", systemImage: "pencil") - } - .tint(.indigo) - } - } - } - } - .navigationTitle("Exercise Types") - .sheet(item: $itemToEdit) {item in - ExerciseTypeAddEditView(model: item) - } - .confirmationDialog( - "Delete?", - isPresented: Binding( - get: { itemToDelete != nil }, - set: { if !$0 { itemToDelete = nil } } - ), - titleVisibility: .visible - ) { - Button("Delete", role: .destructive) { - if let itemToDelete = itemToDelete { - modelContext.delete(itemToDelete) - try? modelContext.save() - self.itemToDelete = nil - } - } - Button("Cancel", role: .cancel) { - itemToDelete = nil - } - } message: { - Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?") - } - } - } -} diff --git a/Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift b/Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift deleted file mode 100644 index 0ae29a7..0000000 --- a/Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift +++ /dev/null @@ -1,96 +0,0 @@ - -import SwiftUI -import SwiftData - -struct ExerciseAddEditView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType] - - @State var model: Exercise - - init(model: Exercise? = nil) { - _model = State(initialValue: model ?? Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30)) - } - - var body: some View { - Form { - 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) - .padding(.vertical, 4) - } - - - Section (header: Text("Setup")) { - TextEditor(text: $model.setup) - .frame(minHeight: 100) - .padding(.vertical, 4) -// } footer: { -// Text("Describe concisely how equipment should be configured") - } - - Section (header: Text("Weight")) { - HStack { - Text("\(model.weight)") - .bold() - Text("lbs") - Spacer() - Stepper("", value: $model.weight, in: 0...1000) - } - } - - - // Section(header: Text("Target Muscles")) { - // Button(action: { - // showingMuscleSelection = true - // }) { - // HStack { - // if selectedMuscles.isEmpty { - // Text("None selected") - // .foregroundColor(.secondary) - // } else { - // Text(selectedMuscles.map { $0.name }.joined(separator: ", ")) - // .foregroundColor(.primary) - // .multilineTextAlignment(.leading) - // } - // Spacer() - // Image(systemName: "chevron.right") - // .foregroundColor(.secondary) - // .font(.caption) - // } - // } - // } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - try? modelContext.save() - dismiss() - } - } - } - - } -} diff --git a/Workouts/Views/Settings/Exercises/ExercisesListView.swift b/Workouts/Views/Settings/Exercises/ExercisesListView.swift deleted file mode 100644 index 8e294c3..0000000 --- a/Workouts/Views/Settings/Exercises/ExercisesListView.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// ExercisesListView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 4:30 PM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI -import SwiftData - -struct ExercisesListView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\ExerciseType.name)]) var groups: [ExerciseType] - - @State var showingAddSheet = false - @State var itemToEdit: Exercise? = nil - @State var itemToDelete: Exercise? = nil - - private func save () { - try? modelContext.save() - } - - var body: some View { - NavigationStack { - Form { - ForEach (groups) { group in - let items = group.exercises ?? [] - let itemCount = items.count - if itemCount > 0 { - Section (header: Text("\(group.name) (\(itemCount))")) { - ForEach (items) { item in - ListItem(title: item.name) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button (role: .destructive) { - itemToDelete = item - } label: { - - Label("Delete", systemImage: "trash") - } - Button { - itemToEdit = item - } label: { - Label("Edit", systemImage: "pencil") - } - .tint(.indigo) - } - } - } - } - } - } - .navigationTitle("Exercises") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - showingAddSheet.toggle() - }) { - Image(systemName: "plus") - } - } - } - .sheet(isPresented: $showingAddSheet) { - ExerciseAddEditView() - } - .sheet(item: $itemToEdit) { item in - ExerciseAddEditView(model: item) - } - .confirmationDialog( - "Delete?", - isPresented: Binding( - get: { itemToDelete != nil }, - set: { if !$0 { itemToDelete = nil } } - ), - titleVisibility: .visible - ) { - Button("Delete", role: .destructive) { - if let itemToDelete = itemToDelete { - modelContext.delete(itemToDelete) - try? modelContext.save() - self.itemToDelete = nil - } - } - Button("Cancel", role: .cancel) { - itemToDelete = nil - } - } message: { - Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?") - } - } - } -} diff --git a/Workouts/Views/Settings/MuscleGroups/MuscleGroupAddEditView.swift b/Workouts/Views/Settings/MuscleGroups/MuscleGroupAddEditView.swift deleted file mode 100644 index cb696b2..0000000 --- a/Workouts/Views/Settings/MuscleGroups/MuscleGroupAddEditView.swift +++ /dev/null @@ -1,55 +0,0 @@ -import SwiftUI - -// -// MuscleGroupAddEditView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 12:14 PM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -struct MuscleGroupAddEditView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @State var model: MuscleGroup - - init(model: MuscleGroup? = nil) { - _model = State(initialValue: model ?? MuscleGroup(name: "", descr: "")) - } - - var body: some View { - NavigationStack { - Form { - Section (header: Text("Name")) { - TextField("Name", text: $model.name) - .bold() - } - - Section(header: Text("Description")) { - TextEditor(text: $model.descr) - .frame(minHeight: 100) - .padding(.vertical, 4) - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - if model.modelContext == nil { - modelContext.insert(model) - } - try? modelContext.save() - dismiss() - } - } - } - } - } -} diff --git a/Workouts/Views/Settings/Muscles/MuscleAddEditView.swift b/Workouts/Views/Settings/Muscles/MuscleAddEditView.swift deleted file mode 100644 index fa1010a..0000000 --- a/Workouts/Views/Settings/Muscles/MuscleAddEditView.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// MuscleAddEditView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 11:55 AM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI -import SwiftData - -struct MuscleAddEditView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup] - @Bindable var model: Muscle - - var body: some View { - NavigationStack { - Form { - 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) - .padding(.vertical, 4) - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - try? modelContext.save() - dismiss() - } - } - } - } - } -} diff --git a/Workouts/Views/Settings/Muscles/MusclesListView.swift b/Workouts/Views/Settings/Muscles/MusclesListView.swift deleted file mode 100644 index 13a7b36..0000000 --- a/Workouts/Views/Settings/Muscles/MusclesListView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// MuscleGroupsListView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 12:14 PM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI -import SwiftData - -struct MusclesListView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\MuscleGroup.name)]) var groups: [MuscleGroup] - - @State var itemToEdit: Muscle? = nil - @State var itemToDelete: Muscle? = nil - - private func save () { - try? modelContext.save() - } - - var body: some View { - NavigationStack { - Form { - ForEach (groups) { group in - Section (header: Text("\(group.name) (\(group.muscles?.count ?? 0))")) { - let items = group.muscles ?? [] - ForEach (items) { item in - ListItem(title: item.name) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button (role: .destructive) { - itemToDelete = item - } label: { - - Label("Delete", systemImage: "trash") - } - Button { - itemToEdit = item - } label: { - Label("Edit", systemImage: "pencil") - } - .tint(.indigo) - } - } - } - } - } - .navigationTitle("Muscles") - .sheet(item: $itemToEdit) { item in - MuscleAddEditView(model: item) - } - .confirmationDialog( - "Delete?", - isPresented: Binding( - get: { itemToDelete != nil }, - set: { if !$0 { itemToDelete = nil } } - ), - titleVisibility: .visible - ) { - Button("Delete", role: .destructive) { - if let itemToDelete = itemToDelete { - modelContext.delete(itemToDelete) - try? modelContext.save() - self.itemToDelete = nil - } - } - Button("Cancel", role: .cancel) { - itemToDelete = nil - } - } message: { - Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?") - } - } - } -} diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/Views/Settings/SettingsView.swift index 29fe35e..5af5cc9 100644 --- a/Workouts/Views/Settings/SettingsView.swift +++ b/Workouts/Views/Settings/SettingsView.swift @@ -75,3 +75,33 @@ 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/Splits/SplitAddEditView.swift b/Workouts/Views/Settings/Splits/SplitAddEditView.swift deleted file mode 100644 index 3fd3b3d..0000000 --- a/Workouts/Views/Settings/Splits/SplitAddEditView.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftUI - -struct SplitAddEditView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Bindable var model: Split - - @State var itemToEdit: SplitExerciseAssignment? = nil - @State var itemToDelete: SplitExerciseAssignment? = nil - - var body: some View { - NavigationStack { - Form { - Section (header: Text("Name")) { - TextField("Name", text: $model.name) - .bold() - } - - Section(header: Text("Description")) { - TextEditor(text: $model.intro) - .frame(minHeight: 100) - .padding(.vertical, 4) - } - - Section(header: Text("Exercises")) { - let item = model - if let assignments = item.exercises, !assignments.isEmpty { - ForEach(assignments, id: \.id) { item in - List { - ListItem( - title: item.exercise?.name ?? "Unnamed", - subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs" - ) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - 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") - .foregroundColor(.secondary) - } - - Button(action: { - - }) { - Label("Add Exercise", systemImage: "plus.circle") - } - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - try? modelContext.save() - dismiss() - } - } - } - } - } -} diff --git a/Workouts/Views/Settings/Splits/SplitsListView.swift b/Workouts/Views/Settings/Splits/SplitsListView.swift deleted file mode 100644 index 566635e..0000000 --- a/Workouts/Views/Settings/Splits/SplitsListView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SplitsListView.swift -// Workouts -// -// Created by rzen on 7/13/25 at 10:27 AM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -import SwiftUI -import SwiftData - -struct SplitsListView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.modelContext) private var modelContext - - @Query(sort: [SortDescriptor(\Split.name)]) var items: [Split] - - @State var itemToEdit: Split? = nil - @State var itemToDelete: Split? = nil - - private func save () { - try? modelContext.save() - } - - var body: some View { - NavigationStack { - Form { - List { - ForEach (items) { item in - ListItem( - title: item.name, - count: item.exercises?.count - ) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button (role: .destructive) { - itemToDelete = item - } label: { - Label("Delete", systemImage: "trash") - } - Button { - itemToEdit = item - } label: { - Label("Edit", systemImage: "pencil") - } - .tint(.indigo) - } - } - } - .navigationTitle("Muscle Groups") - } - .sheet(item: $itemToEdit) {item in - SplitAddEditView(model: item) - } - .confirmationDialog( - "Delete?", - isPresented: Binding( - get: { itemToDelete != nil }, - set: { if !$0 { itemToDelete = nil } } - ), - titleVisibility: .visible - ) { - Button("Delete", role: .destructive) { - if let itemToDelete = itemToDelete { - modelContext.delete(itemToDelete) - try? modelContext.save() - self.itemToDelete = nil - } - } - Button("Cancel", role: .cancel) { - itemToDelete = nil - } - } message: { - Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?") - } - } - } -} diff --git a/Workouts/Views/Workouts/ExercisePickerView.swift b/Workouts/Views/Workouts/ExercisePickerView.swift new file mode 100644 index 0000000..648fe57 --- /dev/null +++ b/Workouts/Views/Workouts/ExercisePickerView.swift @@ -0,0 +1,49 @@ +// +// SplitPickerView.swift +// Workouts +// +// Created by rzen on 7/13/25 at 7:17 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import SwiftData + +struct ExercisePickerView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise] + + var onExerciseSelected: (Exercise) -> Void + + var body: some View { + NavigationStack { + VStack { + Form { + Section (header: Text("This Split")) { + List { + ForEach(exercises) { exercise in + Button(action: { + onExerciseSelected(exercise) + dismiss() + }) { + ListItem(title: exercise.name) + } + .buttonStyle(.plain) + } + } + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + } + } +} diff --git a/Workouts/Views/Workouts/SplitPickerView.swift b/Workouts/Views/Workouts/SplitPickerView.swift new file mode 100644 index 0000000..314660e --- /dev/null +++ b/Workouts/Views/Workouts/SplitPickerView.swift @@ -0,0 +1,73 @@ +// +// SplitPickerView.swift +// Workouts +// +// Created by rzen on 7/13/25 at 7:17 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import SwiftData + +struct SplitPickerView: View { + @Environment(\.modelContext) private var modelContext + @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 { + VStack { + Form { + Section (header: Text("This Split")) { + List { + ForEach(splits) { split in + Button(action: { + onSplitSelected(split) + dismiss() + }) { + HStack { + Text(split.name) + .font(.headline) + Spacer() + Text("\(split.exercises?.count ?? 0)") + .font(.caption) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + } + +// Section (header: Text("Additional Exercises")) { +// List { +// ForEach(exercises) { exercise in +// Button(action: { +// onExerciseSelected(exercise) +// dismiss() +// }) { +// Text(exercise.name) +// } +// .contentShape(Rectangle()) +// .buttonStyle(.plain) +// } +// } +// } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + } + } +} diff --git a/Workouts/Views/Workouts/WorkoutLogEditView.swift b/Workouts/Views/Workouts/WorkoutLogEditView.swift new file mode 100644 index 0000000..4911f42 --- /dev/null +++ b/Workouts/Views/Workouts/WorkoutLogEditView.swift @@ -0,0 +1,103 @@ +// +// WorkoutAddEditView.swift +// Workouts +// +// Created by rzen on 7/13/25 at 9:13 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import SwiftData + +struct WorkoutLogEditView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @State var workoutLog: WorkoutLog + @State private var showingSaveConfirmation = false + + var body: some View { + Form { + Section (header: Text("Exercise")) { + Text("\(workoutLog.exercise?.name ?? "Unnamed Exercise")") + .font(.headline) + } + + Section(header: Text("Sets/Reps")) { + Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10) + Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50) + } + + Section(header: Text("Weight")) { + HStack { + VStack(alignment: .center) { + Text("\(workoutLog.weight) lbs") + .font(.headline) + } + Spacer() + VStack(alignment: .trailing) { + Stepper("±1", value: $workoutLog.weight, in: 0...1000) + Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5) + } + .frame(width: 130) + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + showingSaveConfirmation = true + } + } + } + .confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) { + Button("Save Workout Log Only") { + try? modelContext.save() + dismiss() + } + + Button("Save Workout Log and Update Split") { + // Save the workout log + try? modelContext.save() + + // Update the split with this workout log's data + // Note: Implementation depends on how splits are updated in your app + updateSplit(from: workoutLog) + + dismiss() + } + + Button("Cancel", role: .cancel) { + // Do nothing, dialog will dismiss + } + } + } + + private func updateSplit(from workoutLog: WorkoutLog) { + 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 +// } +// } +// } + } + +} diff --git a/Workouts/Views/Workouts/WorkoutLogView.swift b/Workouts/Views/Workouts/WorkoutLogView.swift new file mode 100644 index 0000000..9bd5d1b --- /dev/null +++ b/Workouts/Views/Workouts/WorkoutLogView.swift @@ -0,0 +1,125 @@ +// +// WorkoutLogView.swift +// Workouts +// +// Created by rzen on 7/13/25 at 6:58 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI + +struct WorkoutLogView: View { + @Environment(\.modelContext) private var modelContext + + @State var workout: Workout + + @State private var showingAddSheet = false + @State private var itemToEdit: WorkoutLog? = nil + @State private var itemToDelete: WorkoutLog? = nil + + var sortedWorkoutLogs: [WorkoutLog] { + if let logs = workout.logs { + logs.sorted(by: { $0.exercise!.name < $1.exercise!.name }) + } else { + [] + } + } + + var body: some View { + Form { + List { + ForEach (sortedWorkoutLogs) { log in + let badges = log.completed ? [Badge(text: "Completed", color: .green)] : [] + ListItem( + title: log.exercise?.name ?? "Untitled Exercise", + subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs", + badges: badges + ) + .swipeActions(edge: .leading, allowsFullSwipe: false) { + if (log.completed) { + Button { + log.completed = false + try? modelContext.save() + } label: { + Label("Complete", systemImage: "circle.fill") + } + .tint(.green) + } else { + Button { + log.completed = true + try? modelContext.save() + } label: { + Label("Reset", systemImage: "checkmark.circle.fill") + } + .tint(.green) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + itemToDelete = log + } label: { + Label("Delete", systemImage: "trash") + } + Button { + itemToEdit = log + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(.indigo) + } + } + } + } + .navigationTitle("Workout") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { showingAddSheet.toggle() }) { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $showingAddSheet) { + ExercisePickerView { exercise in + let workoutLog = WorkoutLog( + workout: workout, + exercise: exercise, + date: Date(), + sets: exercise.sets, + reps: exercise.reps, + weight: exercise.weight, + completed: false + ) + workout.logs?.append(workoutLog) + try? modelContext.save() + } + } + .sheet(item: $itemToEdit) { item in + WorkoutLogEditView(workoutLog: item) + } + .confirmationDialog( + "Delete?", + isPresented: Binding( + get: { itemToDelete != nil }, + set: { if !$0 { itemToDelete = nil } } + ), + titleVisibility: .visible + ) { + Button("Delete", role: .destructive) { + if let item = itemToDelete { + withAnimation { + modelContext.delete(item) + try? modelContext.save() + itemToDelete = nil + } + } + } + Button("Cancel", role: .cancel) { + itemToDelete = nil + } + } message: { + Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?") + } + + } +} diff --git a/Workouts/Views/Workouts/WorkoutView.swift b/Workouts/Views/Workouts/WorkoutView.swift new file mode 100644 index 0000000..b9da228 --- /dev/null +++ b/Workouts/Views/Workouts/WorkoutView.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct WorkoutView: View { + var body: some View { + Text("Workout View") + } +} + +#Preview { + WorkoutView() +} diff --git a/Workouts/Views/Workouts/WorkoutsView.swift b/Workouts/Views/Workouts/WorkoutsView.swift new file mode 100644 index 0000000..96ef67e --- /dev/null +++ b/Workouts/Views/Workouts/WorkoutsView.swift @@ -0,0 +1,150 @@ +// +// WorkoutsView.swift +// Workouts +// +// Created by rzen on 7/13/25 at 6:52 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import SwiftData + +struct WorkoutsView: View { + private let logger = AppLogger( + subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", + category: "WorkoutsView" + ) + + @Environment(\.modelContext) private var modelContext + + @Query(sort: [SortDescriptor(\Workout.start, order: .reverse)]) var workouts: [Workout] + + @State private var showingSplitPicker = false + + @State private var itemToDelete: Workout? = nil + @State private var itemToEdit: Workout? = nil + + var body: some View { + NavigationStack { + Form { + if workouts.isEmpty { + Text("No workouts yet") + } else { + List { + ForEach (workouts) { workout in + NavigationLink(destination: WorkoutLogView(workout: workout)) { + ListItem(title: workout.label) + } + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + itemToDelete = workout + } label: { + Label("Delete", systemImage: "trash") + } + Button { + itemToEdit = workout + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(.indigo) + } + } + } + } + } + .navigationTitle("Workouts") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Start Workout") { + showingSplitPicker = true + } + } + } +// .sheet(item: $itemToEdit) { item in +// T.formView(for: item) +// } + .confirmationDialog( + "Delete?", + isPresented: Binding( + get: { itemToDelete != nil }, + set: { if !$0 { itemToDelete = nil } } + ), + titleVisibility: .visible + ) { + Button("Delete", role: .destructive) { + if let item = itemToDelete { + modelContext.delete(item) + try? modelContext.save() + itemToDelete = nil + } + } + Button("Cancel", role: .cancel) { + itemToDelete = nil + } + } message: { + Text("Are you sure you want to delete this workout?") + } + .sheet(isPresented: $showingSplitPicker) { + 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(), + sets: assignment.sets, + reps: assignment.reps, + weight: assignment.weight, + completed: false + ) + modelContext.insert(workoutLog) + } else { + logger.debug("An exercise entity for a split is nil") + } + } + } + try? modelContext.save() + } + } + } + } +} + +extension Date { + func formattedDate() -> String { + let calendar = Calendar.current + let now = Date() + + let timeFormatter = DateFormatter() + timeFormatter.dateFormat = "h:mm a" + + let dateFormatter = DateFormatter() + + let date = self + + if calendar.isDateInToday(date) { + return "Today @ \(timeFormatter.string(from: date))" + } else if calendar.isDateInYesterday(date) { + return "Yesterday @ \(timeFormatter.string(from: date))" + } else { + let dateComponents = calendar.dateComponents([.year], from: date) + let currentYearComponents = calendar.dateComponents([.year], from: now) + + if dateComponents.year == currentYearComponents.year { + dateFormatter.dateFormat = "M/d" + } else { + dateFormatter.dateFormat = "M/d/yyyy" + } + + let dateString = dateFormatter.string(from: date) + let timeString = timeFormatter.string(from: date) + return "\(dateString) @ \(timeString)" + } + } + +} diff --git a/Workouts/Workouts.entitlements b/Workouts/Workouts.entitlements index cbd408e..a4180f0 100644 --- a/Workouts/Workouts.entitlements +++ b/Workouts/Workouts.entitlements @@ -5,7 +5,9 @@ aps-environment development com.apple.developer.icloud-container-identifiers - + + iCloud.com.dev.rzen.indie.Workouts + com.apple.developer.icloud-services CloudKit