From e3c3f2c6f09366faeeb5a4e7cd0ec0ec07ac2574 Mon Sep 17 00:00:00 2001 From: rzen Date: Sat, 19 Jul 2025 16:42:47 -0400 Subject: [PATCH] wip --- Workouts.xcodeproj/project.pbxproj | 2 + Workouts/ContentView.swift | 10 +- ...xerciseAssignment.swift => Exercise.swift} | 6 +- Workouts/Models/Split.swift | 6 +- Workouts/Models/Workout.swift | 11 +- Workouts/Protocols/ListableItem.swift | 12 -- Workouts/Resources/pf-starter.exercises.yaml | 14 ++ ...outsContainer.swift => AppContainer.swift} | 10 +- Workouts/Schema/CloudKitSyncObserver.swift | 25 +-- Workouts/Schema/SchemaV1.swift | 2 +- Workouts/Schema/SchemaV2.swift | 12 -- Workouts/Schema/SchemaV3.swift | 12 -- Workouts/Schema/SchemaVersion.swift | 16 +- Workouts/Schema/WorkoutsMigrationPlan.swift | 31 ++-- Workouts/Utils/Date+humanTimeInterval.swift | 50 ++++++ .../CalendarListItem.swift | 7 +- .../Common}/CheckboxListItem.swift | 0 .../{Utils => Views/Common}/ListItem.swift | 12 +- .../ExerciseAddEditView.swift} | 12 +- .../Exercises/ExerciseListLoader.swift} | 42 ++--- .../Views/Exercises/ExercisePickerView.swift | 153 ++++++++++++++++++ .../{Splits => Exercises}/ExerciseView.swift | 0 ...sListView.swift => ExerciseListView.swift} | 94 ++++++++--- .../Splits}/OrderableItem.swift | 2 +- Workouts/Views/Splits/SplitAddEditView.swift | 85 ++++++---- ...aggableSplitItem.swift => SplitItem.swift} | 10 +- .../SplitPickerView.swift | 0 Workouts/Views/Splits/SplitsView.swift | 30 ++-- .../WorkoutLogEditView.swift | 10 +- .../WorkoutLogListView.swift} | 63 ++++++-- .../WorkoutLog}/WorkoutStatus.swift | 11 ++ .../Views/Workouts/ExercisePickerView.swift | 92 ----------- Workouts/Views/Workouts/WorkoutEditView.swift | 43 ++--- ...rkoutsView.swift => WorkoutListView.swift} | 34 ++-- Workouts/WorkoutsApp.swift | 2 +- Workouts/{Utils => _ATTIC_}/Badge.swift | 0 Workouts/{Utils => _ATTIC_}/BadgeView.swift | 0 .../Settings => _ATTIC_}/SettingsView.swift | 2 +- 38 files changed, 556 insertions(+), 367 deletions(-) rename Workouts/Models/{SplitExerciseAssignment.swift => Exercise.swift} (79%) delete mode 100644 Workouts/Protocols/ListableItem.swift rename Workouts/Schema/{WorkoutsContainer.swift => AppContainer.swift} (79%) delete mode 100644 Workouts/Schema/SchemaV2.swift delete mode 100644 Workouts/Schema/SchemaV3.swift create mode 100644 Workouts/Utils/Date+humanTimeInterval.swift rename Workouts/Views/{Workouts => Common}/CalendarListItem.swift (91%) rename Workouts/{Utils => Views/Common}/CheckboxListItem.swift (100%) rename Workouts/{Utils => Views/Common}/ListItem.swift (83%) rename Workouts/Views/{Settings/SplitExerciseAssignmentAddEditView.swift => Exercises/ExerciseAddEditView.swift} (85%) rename Workouts/{Models/ExerciseList.swift => Views/Exercises/ExerciseListLoader.swift} (69%) create mode 100644 Workouts/Views/Exercises/ExercisePickerView.swift rename Workouts/Views/{Splits => Exercises}/ExerciseView.swift (100%) rename Workouts/Views/Splits/{SplitExercisesListView.swift => ExerciseListView.swift} (58%) rename Workouts/{Models => Views/Splits}/OrderableItem.swift (92%) rename Workouts/Views/Splits/{DraggableSplitItem.swift => SplitItem.swift} (82%) rename Workouts/Views/{Workouts => Splits}/SplitPickerView.swift (100%) rename Workouts/Views/{Workouts => WorkoutLog}/WorkoutLogEditView.swift (91%) rename Workouts/Views/{Workouts/WorkoutLogView.swift => WorkoutLog/WorkoutLogListView.swift} (80%) rename Workouts/{Models => Views/WorkoutLog}/WorkoutStatus.swift (66%) delete mode 100644 Workouts/Views/Workouts/ExercisePickerView.swift rename Workouts/Views/Workouts/{WorkoutsView.swift => WorkoutListView.swift} (83%) rename Workouts/{Utils => _ATTIC_}/Badge.swift (100%) rename Workouts/{Utils => _ATTIC_}/BadgeView.swift (100%) rename Workouts/{Views/Settings => _ATTIC_}/SettingsView.swift (97%) diff --git a/Workouts.xcodeproj/project.pbxproj b/Workouts.xcodeproj/project.pbxproj index 4c11185..b901763 100644 --- a/Workouts.xcodeproj/project.pbxproj +++ b/Workouts.xcodeproj/project.pbxproj @@ -347,6 +347,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; + EXCLUDED_SOURCE_FILE_NAMES = "_ATTIC_/*"; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -410,6 +411,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; + EXCLUDED_SOURCE_FILE_NAMES = "_ATTIC_/*"; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Workouts/ContentView.swift b/Workouts/ContentView.swift index 5208368..480fb8d 100644 --- a/Workouts/ContentView.swift +++ b/Workouts/ContentView.swift @@ -21,7 +21,7 @@ struct ContentView: View { Label("Workouts", systemImage: "figure.strengthtraining.traditional") } - WorkoutsView() + WorkoutListView() .tabItem { Label("Logs", systemImage: "list.bullet.clipboard.fill") } @@ -36,10 +36,10 @@ struct ContentView: View { Label("Reports", systemImage: "chart.bar") } - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } +// SettingsView() +// .tabItem { +// Label("Settings", systemImage: "gear") +// } } .observeCloudKitChanges() } diff --git a/Workouts/Models/SplitExerciseAssignment.swift b/Workouts/Models/Exercise.swift similarity index 79% rename from Workouts/Models/SplitExerciseAssignment.swift rename to Workouts/Models/Exercise.swift index 5be8de5..26ca85e 100644 --- a/Workouts/Models/SplitExerciseAssignment.swift +++ b/Workouts/Models/Exercise.swift @@ -2,8 +2,8 @@ import Foundation import SwiftData @Model -final class SplitExerciseAssignment { - var exerciseName: String = "" +final class Exercise { + var name: String = "" var order: Int = 0 var sets: Int = 0 var reps: Int = 0 @@ -14,7 +14,7 @@ final class SplitExerciseAssignment { init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) { self.split = split - self.exerciseName = exerciseName + self.name = exerciseName self.order = order self.sets = sets self.reps = reps diff --git a/Workouts/Models/Split.swift b/Workouts/Models/Split.swift index 766f132..be6e6ab 100644 --- a/Workouts/Models/Split.swift +++ b/Workouts/Models/Split.swift @@ -15,8 +15,8 @@ final class Split { return Color.color(from: self.color) } - @Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split) - var exercises: [SplitExerciseAssignment]? = [] + @Relationship(deleteRule: .cascade, inverse: \Exercise.split) + var exercises: [Exercise]? = [] @Relationship(deleteRule: .nullify, inverse: \Workout.split) var workouts: [Workout]? = [] @@ -109,7 +109,7 @@ fileprivate struct SplitFormView: View { Section(header: Text("Exercises")) { NavigationLink { - SplitExercisesListView(model: model) + ExerciseListView(split: model) } label: { ListItem( text: "Exercises", diff --git a/Workouts/Models/Workout.swift b/Workouts/Models/Workout.swift index ad7fc35..a2b72f6 100644 --- a/Workouts/Models/Workout.swift +++ b/Workouts/Models/Workout.swift @@ -5,20 +5,25 @@ import SwiftData final class Workout { var start: Date = Date() var end: Date? - + var status: WorkoutStatus? = WorkoutStatus.notStarted + @Relationship(deleteRule: .nullify) var split: Split? @Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout) var logs: [WorkoutLog]? = [] - init(start: Date, end: Date? = nil, split: Split?) { + init(start: Date, end: Date, split: Split?) { self.start = start self.end = end self.split = split } var label: String { - start.formattedDate() + if status == .completed, let endDate = end { + return "\(start.formattedDate())—\(endDate.formattedDate())" + } else { + return start.formattedDate() + } } } diff --git a/Workouts/Protocols/ListableItem.swift b/Workouts/Protocols/ListableItem.swift deleted file mode 100644 index 5eab74a..0000000 --- a/Workouts/Protocols/ListableItem.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ListableItem.swift -// Workouts -// -// Created by rzen on 7/13/25 at 10:40 AM. -// -// Copyright 2025 Rouslan Zenetl. All Rights Reserved. -// - -protocol ListableItem { - var name: String { get set } -} diff --git a/Workouts/Resources/pf-starter.exercises.yaml b/Workouts/Resources/pf-starter.exercises.yaml index 54814c5..7cb829d 100644 --- a/Workouts/Resources/pf-starter.exercises.yaml +++ b/Workouts/Resources/pf-starter.exercises.yaml @@ -6,64 +6,78 @@ exercises: shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades together. Avoid leaning back excessively or using momentum. type: Machine-Based + split: Upper Body - 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 + split: Upper Body - 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 + split: Upper Body - 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 + split: Upper Body - 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 + split: Upper Body - 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 + split: Upper Body - 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 + split: Core - 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 + split: Core - 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 + split: Lower Body - 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 + split: Lower Body - 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 + split: Lower Body - 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 + split: Lower Body - 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 + split: Lower Body - 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 + split: Lower Body diff --git a/Workouts/Schema/WorkoutsContainer.swift b/Workouts/Schema/AppContainer.swift similarity index 79% rename from Workouts/Schema/WorkoutsContainer.swift rename to Workouts/Schema/AppContainer.swift index 87df3d2..d4e041f 100644 --- a/Workouts/Schema/WorkoutsContainer.swift +++ b/Workouts/Schema/AppContainer.swift @@ -1,15 +1,15 @@ import Foundation import SwiftData -final class WorkoutsContainer { +final class AppContainer { static let logger = AppLogger( subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", - category: "WorkoutsContainer" + category: "AppContainer" ) static func create() -> ModelContainer { // Using the current models directly without migration plan to avoid reference errors - let schema = Schema(SchemaV1.models) + let schema = Schema(SchemaVersion.models) let configuration = ModelConfiguration(cloudKitDatabase: .automatic) let container = try! ModelContainer(for: schema, configurations: configuration) return container @@ -20,10 +20,8 @@ final class WorkoutsContainer { let configuration = ModelConfiguration(isStoredInMemoryOnly: true) do { - let schema = Schema(SchemaV1.models) + let schema = Schema(SchemaVersion.models) let container = try ModelContainer(for: schema, configurations: configuration) - let context = ModelContext(container) - return container } catch { fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)") diff --git a/Workouts/Schema/CloudKitSyncObserver.swift b/Workouts/Schema/CloudKitSyncObserver.swift index 92b520f..e3f947b 100644 --- a/Workouts/Schema/CloudKitSyncObserver.swift +++ b/Workouts/Schema/CloudKitSyncObserver.swift @@ -12,23 +12,14 @@ struct CloudKitSyncObserver: ViewModifier { .onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in refreshID = UUID() Task { @MainActor in -// do { -// for entity in modelContext.container.schema.entities { -// fetchAll(of: entity.Type, from: modelContext) -// } -// } catch { -// print("ERROR: failed to fetch data on CloudKit change") -// } -// -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) -// try? modelContext.fetch(FetchDescriptor()) - // TODO: add more entities? + do { + let _ = try await fetchAll(of: Exercise.self, from: modelContext) + let _ = try await fetchAll(of: Split.self, from: modelContext) + let _ = try await fetchAll(of: Workout.self, from: modelContext) + let _ = try await fetchAll(of: WorkoutLog.self, from: modelContext) + } catch { + print("ERROR: failed to fetch \(error.localizedDescription)") + } } } } diff --git a/Workouts/Schema/SchemaV1.swift b/Workouts/Schema/SchemaV1.swift index f265ed6..3956d98 100644 --- a/Workouts/Schema/SchemaV1.swift +++ b/Workouts/Schema/SchemaV1.swift @@ -5,7 +5,7 @@ enum SchemaV1: VersionedSchema { static var models: [any PersistentModel.Type] = [ Split.self, - SplitExerciseAssignment.self, + Exercise.self, Workout.self, WorkoutLog.self ] diff --git a/Workouts/Schema/SchemaV2.swift b/Workouts/Schema/SchemaV2.swift deleted file mode 100644 index f3d9642..0000000 --- a/Workouts/Schema/SchemaV2.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftData - -enum SchemaV2: VersionedSchema { - static var versionIdentifier: Schema.Version = .init(1, 0, 1) - - static var models: [any PersistentModel.Type] = [ - Split.self, - SplitExerciseAssignment.self, - Workout.self, - WorkoutLog.self - ] -} diff --git a/Workouts/Schema/SchemaV3.swift b/Workouts/Schema/SchemaV3.swift deleted file mode 100644 index 076a542..0000000 --- a/Workouts/Schema/SchemaV3.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftData - -enum SchemaV3: VersionedSchema { - static var versionIdentifier: Schema.Version = .init(1, 0, 2) - - static var models: [any PersistentModel.Type] = [ - Split.self, - SplitExerciseAssignment.self, - Workout.self, - WorkoutLog.self - ] -} diff --git a/Workouts/Schema/SchemaVersion.swift b/Workouts/Schema/SchemaVersion.swift index 4561c69..2fa025b 100644 --- a/Workouts/Schema/SchemaVersion.swift +++ b/Workouts/Schema/SchemaVersion.swift @@ -2,8 +2,18 @@ import SwiftData enum SchemaVersion: Int { case v1 - case v2 - case v3 - static var current: SchemaVersion { .v3 } + static var current: SchemaVersion { .v1 } + + static var schemas: [VersionedSchema.Type] { + [ + SchemaV1.self + ] + } + + static var models: [any PersistentModel.Type] { + switch (Self.current) { + case .v1: SchemaV1.models + } + } } diff --git a/Workouts/Schema/WorkoutsMigrationPlan.swift b/Workouts/Schema/WorkoutsMigrationPlan.swift index 030d51f..82e4d8e 100644 --- a/Workouts/Schema/WorkoutsMigrationPlan.swift +++ b/Workouts/Schema/WorkoutsMigrationPlan.swift @@ -1,24 +1,29 @@ import SwiftData +import Foundation struct WorkoutsMigrationPlan: SchemaMigrationPlan { - static var schemas: [VersionedSchema.Type] = [ - SchemaV1.self, - SchemaV2.self - ] + static var schemas: [VersionedSchema.Type] = SchemaVersion.schemas static var stages: [MigrationStage] = [ - // Migration from V1 to V2: Add status field to WorkoutLog MigrationStage.custom( fromVersion: SchemaV1.self, - toVersion: SchemaV2.self, + toVersion: SchemaV1.self, willMigrate: { context in - // Get all WorkoutLog instances - let workoutLogs = try? context.fetch(FetchDescriptor()) - - // Update each WorkoutLog with appropriate status based on completed flag - workoutLogs?.forEach { workoutLog in - // If completed is true, set status to .completed, otherwise set to .notStarted - workoutLog.status = workoutLog.completed ? WorkoutStatus.completed : WorkoutStatus.notStarted + print("migrating from v1 to v1") + let workouts = try? context.fetch(FetchDescriptor()) + workouts?.forEach { workout in + if let status = workout.status { + + } else { + workout.status = .notStarted + } + +// if let endDate = workout.end { +// +// } else { +// workout.end = Date() +// } + workout.end = Date() } }, didMigrate: { _ in diff --git a/Workouts/Utils/Date+humanTimeInterval.swift b/Workouts/Utils/Date+humanTimeInterval.swift new file mode 100644 index 0000000..5902d44 --- /dev/null +++ b/Workouts/Utils/Date+humanTimeInterval.swift @@ -0,0 +1,50 @@ +// +// Date+humanTimeInterval.swift +// Workouts +// +// Created by rzen on 7/19/25 at 1:06 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import Foundation + +extension Date { + func humanTimeInterval(to referenceDate: Date = Date()) -> String { + let seconds = Int(referenceDate.timeIntervalSince(self)) + let absSeconds = abs(seconds) + + let minute = 60 + let hour = 3600 + let day = 86400 + let week = 604800 + let month = 2592000 + let year = 31536000 + + switch absSeconds { + case 0..<5: + return "just now" + case 5.. [String: ExerciseList] { - var exerciseLists: [String: ExerciseList] = [:] + struct ExerciseListData: Codable { + let name: String + let source: String + let exercises: [ExerciseItem] + + struct ExerciseItem: Codable, Identifiable { + let name: String + let descr: String + let type: String + let split: String + + var id: String { name } + } + } + + static func loadExerciseLists() -> [String: ExerciseListData] { + var exerciseLists: [String: ExerciseListData] = [:] guard let resourcePath = Bundle.main.resourcePath else { print("Could not find resource path") @@ -39,18 +40,19 @@ class ExerciseListLoader { let source = exerciseList["source"] as? String, let exercisesData = exerciseList["exercises"] as? [[String: Any]] { - var exercises: [ExerciseList.ExerciseItem] = [] + var exercises: [ExerciseListData.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) + let type = exerciseData["type"] as? String, + let split = exerciseData["split"] as? String { + let exercise = ExerciseListData.ExerciseItem(name: name, descr: descr, type: type, split: split) exercises.append(exercise) } } - let exerciseList = ExerciseList(name: name, source: source, exercises: exercises) + let exerciseList = ExerciseListData(name: name, source: source, exercises: exercises) exerciseLists[fileName] = exerciseList } } catch { diff --git a/Workouts/Views/Exercises/ExercisePickerView.swift b/Workouts/Views/Exercises/ExercisePickerView.swift new file mode 100644 index 0000000..61e120c --- /dev/null +++ b/Workouts/Views/Exercises/ExercisePickerView.swift @@ -0,0 +1,153 @@ +// +// ExercisePickerView.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(\.dismiss) private var dismiss + @State private var exerciseLists: [String: ExerciseListLoader.ExerciseListData] = [:] + @State private var selectedListName: String? = nil + @State private var selectedExercises: Set = [] + + var onExerciseSelected: ([String]) -> Void + var allowMultiSelect: Bool = false + + init(onExerciseSelected: @escaping ([String]) -> Void, allowMultiSelect: Bool = false) { + self.onExerciseSelected = onExerciseSelected + self.allowMultiSelect = allowMultiSelect + } + + var body: some View { + NavigationStack { + Group { + Text("Multi-Select: \(allowMultiSelect)") + 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 grouped by split + List { + // Group exercises by split + let exercisesByGroup = Dictionary(grouping: list.exercises) { $0.split } + let sortedGroups = exercisesByGroup.keys.sorted() + + ForEach(sortedGroups, id: \.self) { splitName in + Section(header: Text(splitName)) { + ForEach(exercisesByGroup[splitName]?.sorted(by: { $0.name < $1.name }) ?? [], id: \.id) { exercise in + if allowMultiSelect { + Button(action: { + if selectedExercises.contains(exercise.name) { + selectedExercises.remove(exercise.name) + } else { + selectedExercises.insert(exercise.name) + } + }) { + HStack { + VStack(alignment: .leading) { + Text(exercise.name) + .font(.headline) + Text(exercise.type) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 2) + + Spacer() + + if selectedExercises.contains(exercise.name) { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } else { + 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 { + if let _ = selectedListName { + ToolbarItem(placement: .navigationBarLeading) { + Button("Back") { + selectedListName = nil + selectedExercises.removeAll() + } + } + } + + if allowMultiSelect { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Select") { + if !selectedExercises.isEmpty { + onExerciseSelected(Array(selectedExercises)) + dismiss() + } + } + .disabled(selectedExercises.isEmpty) + } + } + } + } + } + .toolbar { + if let _ = selectedListName { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + } + } + .onAppear { + loadExerciseLists() + } + } + + private func loadExerciseLists() { + exerciseLists = ExerciseListLoader.loadExerciseLists() + } +} diff --git a/Workouts/Views/Splits/ExerciseView.swift b/Workouts/Views/Exercises/ExerciseView.swift similarity index 100% rename from Workouts/Views/Splits/ExerciseView.swift rename to Workouts/Views/Exercises/ExerciseView.swift diff --git a/Workouts/Views/Splits/SplitExercisesListView.swift b/Workouts/Views/Splits/ExerciseListView.swift similarity index 58% rename from Workouts/Views/Splits/SplitExercisesListView.swift rename to Workouts/Views/Splits/ExerciseListView.swift index f3e488a..a2ce30d 100644 --- a/Workouts/Views/Splits/SplitExercisesListView.swift +++ b/Workouts/Views/Splits/ExerciseListView.swift @@ -10,35 +10,51 @@ import SwiftUI import SwiftData -struct SplitExercisesListView: View { +struct ExerciseListView: View { @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss - var model: Split + // Use a @Query to observe the Split and its exercises + @Query private var splits: [Split] + private var split: Split @State private var showingAddSheet: Bool = false - @State private var itemToEdit: SplitExerciseAssignment? = nil - @State private var itemToDelete: SplitExerciseAssignment? = nil + @State private var itemToEdit: Exercise? = nil + @State private var itemToDelete: Exercise? = nil @State private var createdWorkout: Workout? = nil + + // Initialize with a Split and set up a query to observe it + init(split: Split) { + self.split = split + // Create a predicate to fetch only this specific split + let splitId = split.persistentModelID + self._splits = Query(filter: #Predicate { s in + s.persistentModelID == splitId + }) + } var body: some View { + // Use the first Split from our query if available, otherwise fall back to the original split + let currentSplit = splits.first ?? split + 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 }) + if let assignments = currentSplit.exercises, !assignments.isEmpty { + let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.name < $1.name : $0.order < $1.order }) ForEach(sortedAssignments) { item in ListItem( - title: item.exerciseName, + title: item.name, subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)" ) .swipeActions { Button { itemToDelete = item } label: { - Label("Delete", systemImage: "circle") + Label("Delete", systemImage: "trash") } + .tint(.red) Button { itemToEdit = item } label: { @@ -76,19 +92,19 @@ struct SplitExercisesListView: View { } } } - .navigationTitle("\(model.name)") + .navigationTitle("\(currentSplit.name)") } .toolbar { ToolbarItem(placement: .primaryAction) { Button("Start This Split") { - let split = model - let workout = Workout(start: Date(), split: split) + let split = currentSplit + let workout = Workout(start: Date(), end: Date(), split: split) modelContext.insert(workout) if let exercises = split.exercises { for assignment in exercises { let workoutLog = WorkoutLog( workout: workout, - exerciseName: assignment.exerciseName, + exerciseName: assignment.name, date: Date(), order: assignment.order, sets: assignment.sets, @@ -106,7 +122,7 @@ struct SplitExercisesListView: View { } } .navigationDestination(item: $createdWorkout, destination: { workout in - WorkoutLogView(workout: workout) + WorkoutLogListView(workout: workout) }) // .toolbar { // ToolbarItem(placement: .navigationBarTrailing) { @@ -116,19 +132,49 @@ struct SplitExercisesListView: View { // } // } .sheet (isPresented: $showingAddSheet) { - ExercisePickerView { exerciseName in - itemToEdit = SplitExerciseAssignment( - split: model, - exerciseName: exerciseName, - order: 0, - sets: 3, - reps: 10, - weight: 40 - ) - } + ExercisePickerView(onExerciseSelected: { exerciseNames in + let splitId = currentSplit.persistentModelID + print("exerciseNames: \(exerciseNames)") + if exerciseNames.count == 1 { + itemToEdit = Exercise( + split: split, + exerciseName: exerciseNames.first ?? "Exercise.unnamed", + order: 0, + sets: 3, + reps: 10, + weight: 40 + ) + } else { + for exerciseName in exerciseNames { + var duplicateExercise: [Exercise]? = nil + do { + duplicateExercise = try modelContext.fetch(FetchDescriptor(predicate: #Predicate{ exercise in + exerciseName == exercise.name && splitId == exercise.split?.persistentModelID + })) + } catch { + print("ERROR: failed to fetch \(exerciseName)") + } + + if let dup = duplicateExercise, dup.count > 0 { + print("Skipping duplicate \(exerciseName) found \(dup.count) duplicate(s)") + } else { + print("Creating \(exerciseName) for \(split.name)") + modelContext.insert(Exercise( + split: split, + exerciseName: exerciseName, + order: 0, + sets: 3, + reps: 10, + weight: 40 + )) + } + } + } + try? modelContext.save() + }, allowMultiSelect: true) } .sheet(item: $itemToEdit) { item in - SplitExerciseAssignmentAddEditView(model: item) + ExerciseAddEditView(model: item) } .confirmationDialog( "Delete Exercise?", diff --git a/Workouts/Models/OrderableItem.swift b/Workouts/Views/Splits/OrderableItem.swift similarity index 92% rename from Workouts/Models/OrderableItem.swift rename to Workouts/Views/Splits/OrderableItem.swift index 0ba0b9f..dd8d485 100644 --- a/Workouts/Models/OrderableItem.swift +++ b/Workouts/Views/Splits/OrderableItem.swift @@ -23,7 +23,7 @@ extension Split: OrderableItem { } /// Extension to make SplitExerciseAssignment conform to OrderableItem -extension SplitExerciseAssignment: OrderableItem { +extension Exercise: OrderableItem { func updateOrder(to index: Int) { self.order = index } diff --git a/Workouts/Views/Splits/SplitAddEditView.swift b/Workouts/Views/Splits/SplitAddEditView.swift index d43d90e..d2d57b1 100644 --- a/Workouts/Views/Splits/SplitAddEditView.swift +++ b/Workouts/Views/Splits/SplitAddEditView.swift @@ -10,6 +10,9 @@ import SwiftUI struct SplitAddEditView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + @State var model: Split private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"] @@ -17,46 +20,62 @@ struct SplitAddEditView: View { 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) + NavigationStack { + 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) } - .tag(colorName) } } - Picker("Icon", selection: $model.systemImage) { - ForEach(availableIcons, id: \.self) { iconName in - HStack { - Image(systemName: iconName) - .frame(width: 24, height: 24) - Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized) - } - .tag(iconName) + Section(header: Text("Exercises")) { + NavigationLink { + ExerciseListView(split: model) + } label: { + ListItem( + text: "Exercises", + count: model.exercises?.count ?? 0 + ) } } } - - Section(header: Text("Exercises")) { - NavigationLink { - SplitExercisesListView(model: model) - } label: { - ListItem( - text: "Exercises", - count: model.exercises?.count ?? 0 - ) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + try? modelContext.save() + dismiss() + } } } } diff --git a/Workouts/Views/Splits/DraggableSplitItem.swift b/Workouts/Views/Splits/SplitItem.swift similarity index 82% rename from Workouts/Views/Splits/DraggableSplitItem.swift rename to Workouts/Views/Splits/SplitItem.swift index 867531c..d9536ae 100644 --- a/Workouts/Views/Splits/DraggableSplitItem.swift +++ b/Workouts/Views/Splits/SplitItem.swift @@ -9,7 +9,7 @@ import SwiftUI -struct DraggableSplitItem: View { +struct SplitItem: View { var name: String var color: Color @@ -31,15 +31,15 @@ struct DraggableSplitItem: View { .aspectRatio(1.618, contentMode: .fit) .shadow(radius: 2) - GeometryReader { geometry in + GeometryReader { geo in VStack(spacing: 4) { Spacer() // Icon in the center - now using dynamic sizing Image(systemName: systemImageName) - .font(.system(size: min(geometry.size.width * 0.3, 40), weight: .bold)) + .font(.system(size: min(geo.size.width * 0.3, 40), weight: .bold)) .scaledToFit() - .frame(maxWidth: geometry.size.width * 0.6, maxHeight: geometry.size.height * 0.4) + .frame(maxWidth: geo.size.width * 0.6, maxHeight: geo.size.height * 0.4) .padding(.bottom, 4) // Name at the bottom inside the rectangle @@ -53,7 +53,7 @@ struct DraggableSplitItem: View { .padding(.bottom, 8) } .foregroundColor(.white) - .frame(width: geometry.size.width, height: geometry.size.height) + .frame(width: geo.size.width, height: geo.size.height) } } } diff --git a/Workouts/Views/Workouts/SplitPickerView.swift b/Workouts/Views/Splits/SplitPickerView.swift similarity index 100% rename from Workouts/Views/Workouts/SplitPickerView.swift rename to Workouts/Views/Splits/SplitPickerView.swift diff --git a/Workouts/Views/Splits/SplitsView.swift b/Workouts/Views/Splits/SplitsView.swift index 5c1632b..2ee1e99 100644 --- a/Workouts/Views/Splits/SplitsView.swift +++ b/Workouts/Views/Splits/SplitsView.swift @@ -25,9 +25,9 @@ struct SplitsView: View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) { SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in NavigationLink { - SplitExercisesListView(model: split) + ExerciseListView(split: split) } label: { - DraggableSplitItem( + SplitItem( name: split.name, color: Color.color(from: split.color), systemImageName: split.systemImage, @@ -40,18 +40,7 @@ struct SplitsView: View { .padding() } .navigationTitle("Splits") - .onAppear { - do { - self.splits = try modelContext.fetch(FetchDescriptor( - sortBy: [ - SortDescriptor(\Split.order), - SortDescriptor(\Split.name) - ] - )) - } catch { - print("ERROR: failed to load splits \(error)") - } - } + .onAppear(perform: loadSplits) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { showingAddSheet.toggle() }) { @@ -65,4 +54,17 @@ struct SplitsView: View { } } + + func loadSplits () { + do { + self.splits = try modelContext.fetch(FetchDescriptor( + sortBy: [ + SortDescriptor(\Split.order), + SortDescriptor(\Split.name) + ] + )) + } catch { + print("ERROR: failed to load splits \(error)") + } + } } diff --git a/Workouts/Views/Workouts/WorkoutLogEditView.swift b/Workouts/Views/WorkoutLog/WorkoutLogEditView.swift similarity index 91% rename from Workouts/Views/Workouts/WorkoutLogEditView.swift rename to Workouts/Views/WorkoutLog/WorkoutLogEditView.swift index ac8741e..3b717d4 100644 --- a/Workouts/Views/Workouts/WorkoutLogEditView.swift +++ b/Workouts/Views/WorkoutLog/WorkoutLogEditView.swift @@ -87,12 +87,12 @@ struct WorkoutLogEditView: View { // Find the matching exercise in split.exercises by name if let exercises = split?.exercises { - for exerciseAssignment in exercises { - if exerciseAssignment.exerciseName == workoutLog.exerciseName { + for exercise in exercises { + if exercise.name == 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 + exercise.sets = workoutLog.sets + exercise.reps = workoutLog.reps + exercise.weight = workoutLog.weight // Save the changes to the split try? modelContext.save() diff --git a/Workouts/Views/Workouts/WorkoutLogView.swift b/Workouts/Views/WorkoutLog/WorkoutLogListView.swift similarity index 80% rename from Workouts/Views/Workouts/WorkoutLogView.swift rename to Workouts/Views/WorkoutLog/WorkoutLogListView.swift index 7e10f92..6314c84 100644 --- a/Workouts/Views/Workouts/WorkoutLogView.swift +++ b/Workouts/Views/WorkoutLog/WorkoutLogListView.swift @@ -10,7 +10,7 @@ import SwiftUI import SwiftData -struct WorkoutLogView: View { +struct WorkoutLogListView: View { @Environment(\.modelContext) private var modelContext @State var workout: Workout @@ -47,10 +47,7 @@ struct WorkoutLogView: View { if [.inProgress,.completed].contains(status) { Button { - withAnimation { - log.status = .notStarted - try? modelContext.save() - } + resetWorkout(log) } label: { Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName) } @@ -59,10 +56,7 @@ struct WorkoutLogView: View { if [.notStarted,.completed].contains(status) { Button { - withAnimation { - log.status = .inProgress - try? modelContext.save() - } + startWorkout(log) } label: { Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName) } @@ -71,10 +65,7 @@ struct WorkoutLogView: View { if [.notStarted,.inProgress].contains(status) { Button { - withAnimation { - log.status = .completed - try? modelContext.save() - } + completeWorkout(log) } label: { Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName) } @@ -110,11 +101,11 @@ struct WorkoutLogView: View { } } .sheet(isPresented: $showingAddSheet) { - ExercisePickerView { exerciseName in - let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext) + ExercisePickerView { exerciseNames in + let setsRepsWeight = getSetsRepsWeight(exerciseNames.first ?? "Exercise.unnamed", in: modelContext) let workoutLog = WorkoutLog( workout: workout, - exerciseName: exerciseName, + exerciseName: exerciseNames.first ?? "Exercise.unnamed", date: Date(), sets: setsRepsWeight.sets, reps: setsRepsWeight.reps, @@ -154,6 +145,46 @@ struct WorkoutLogView: View { } + func startWorkout (_ log: WorkoutLog) { + withAnimation { + log.status = .inProgress + updateWorkout(log) + } + } + + func resetWorkout (_ log: WorkoutLog) { + withAnimation { + log.status = .notStarted + updateWorkout(log) + } + } + + func completeWorkout (_ log: WorkoutLog) { + withAnimation { + log.status = .completed + updateWorkout(log) + } + } + + func updateWorkout (_ log: WorkoutLog) { + if let workout = log.workout { + if let _ = workout.logs?.first(where: { $0.status != .completed }) { + if let notStartedLogs = workout.logs?.filter({ $0.status == .notStarted }) { + if notStartedLogs.count == workout.logs?.count ?? 0 { + workout.status = .notStarted + } + } + if let _ = workout.logs?.first(where: { $0.status == .inProgress }) { + workout.status = .inProgress + } + } else { + workout.status = .completed + workout.end = Date() + } + try? modelContext.save() + } + } + func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight { // Use a single expression predicate that works with SwiftData print("Searching for exercise name: \(exerciseName)") diff --git a/Workouts/Models/WorkoutStatus.swift b/Workouts/Views/WorkoutLog/WorkoutStatus.swift similarity index 66% rename from Workouts/Models/WorkoutStatus.swift rename to Workouts/Views/WorkoutLog/WorkoutStatus.swift index c249f40..cace670 100644 --- a/Workouts/Models/WorkoutStatus.swift +++ b/Workouts/Views/WorkoutLog/WorkoutStatus.swift @@ -13,6 +13,17 @@ enum WorkoutStatus: Int, Codable { case completed = 3 case skipped = 4 + static var unnamed = "Undetermined" + + var name: String { + switch (self) { + case .notStarted: "Not Started" + case .inProgress: "In Progress" + case .completed: "Completed" + case .skipped: "Skipped" + } + } + var checkboxStatus: CheckboxStatus { switch (self) { case .notStarted: .unchecked diff --git a/Workouts/Views/Workouts/ExercisePickerView.swift b/Workouts/Views/Workouts/ExercisePickerView.swift deleted file mode 100644 index 63abe8f..0000000 --- a/Workouts/Views/Workouts/ExercisePickerView.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ExercisePickerView.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(\.dismiss) private var dismiss - @State private var exerciseLists: [String: ExerciseList] = [:] - @State private var selectedListName: String? = nil - - var onExerciseSelected: (String) -> Void - - var body: some View { - NavigationStack { - 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 - } - } - } - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - } - } - .onAppear { - loadExerciseLists() - } - } - - private func loadExerciseLists() { - exerciseLists = ExerciseListLoader.loadExerciseLists() - } -} diff --git a/Workouts/Views/Workouts/WorkoutEditView.swift b/Workouts/Views/Workouts/WorkoutEditView.swift index 1e5b7c4..93bf519 100644 --- a/Workouts/Views/Workouts/WorkoutEditView.swift +++ b/Workouts/Views/Workouts/WorkoutEditView.swift @@ -20,44 +20,20 @@ struct WorkoutEditView: View { var body: some View { NavigationStack { Form { -// Section (header: Text("Split")) { -// Text("\(workout.split?.name ?? Split.unnamed)") -// } + Section (header: Text("Split")) { + Text("\(workout.split?.name ?? Split.unnamed)") + } + + Section (header: Text("Status")) { + Text("\(workout.status?.name ?? WorkoutStatus.unnamed)") + } Section (header: Text("Start/End")) { DatePicker("Started", selection: $workout.start) - Toggle("Workout Ended", isOn: Binding( - get: { workout.end != nil }, - set: { newValue in - withAnimation { - if newValue { - workoutEndDate = Date() - workout.end = workoutEndDate - } else { - workout.end = nil - } - } - } - )) - if workout.end != nil { + if workout.status == .completed { DatePicker("Ended", selection: $workoutEndDate) } } - -// Section (header: Text("Workout Log")) { -// if let workoutLogs = workout.logs { -// List { -// ForEach (workoutLogs) { log in -// ListItem( -// title: log.exerciseName, -// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs" -// ) -// } -// } -// } else { -// Text("No workout logs yet") -// } -// } } .navigationTitle("\(workout.split?.name ?? Split.unnamed) Split") .toolbar { @@ -70,6 +46,9 @@ struct WorkoutEditView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { try? modelContext.save() + if workout.status == .completed { + workout.end = workoutEndDate + } dismiss() } } diff --git a/Workouts/Views/Workouts/WorkoutsView.swift b/Workouts/Views/Workouts/WorkoutListView.swift similarity index 83% rename from Workouts/Views/Workouts/WorkoutsView.swift rename to Workouts/Views/Workouts/WorkoutListView.swift index 578bff2..edead44 100644 --- a/Workouts/Views/Workouts/WorkoutsView.swift +++ b/Workouts/Views/Workouts/WorkoutListView.swift @@ -10,7 +10,7 @@ import SwiftUI import SwiftData -struct WorkoutsView: View { +struct WorkoutListView: View { private let logger = AppLogger( subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", category: "WorkoutsView" @@ -33,11 +33,12 @@ struct WorkoutsView: View { } else { List { ForEach (workouts) { workout in - NavigationLink(destination: WorkoutLogView(workout: workout)) { + NavigationLink(destination: WorkoutLogListView(workout: workout)) { CalendarListItem( date: workout.start, title: workout.split?.name ?? Split.unnamed, - subtitle: workout.label + subtitle: "\(workout.status == .completed ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.status?.name ?? WorkoutStatus.unnamed)" )", + subtitle2: "\(workout.status?.name ?? WorkoutStatus.unnamed)" ) } .swipeActions(edge: .trailing, allowsFullSwipe: false) { @@ -59,16 +60,9 @@ struct WorkoutsView: View { } } .navigationTitle("Workouts") -// .toolbar { -// ToolbarItem(placement: .primaryAction) { -// Button("Start Workout") { -// showingSplitPicker = true -// } -// } -// } -// .sheet(item: $itemToEdit) { item in -// WorkoutEditView(workout: item) -// } + .sheet(item: $itemToEdit) { item in + WorkoutEditView(workout: item) + } .confirmationDialog( "Delete?", isPresented: Binding( @@ -94,18 +88,18 @@ struct WorkoutsView: View { } .sheet(isPresented: $showingSplitPicker) { SplitPickerView { split in - let workout = Workout(start: Date(), split: split) + let workout = Workout(start: Date(), end: Date(), split: split) modelContext.insert(workout) if let exercises = split.exercises { - for assignment in exercises { + for exercise in exercises { let workoutLog = WorkoutLog( workout: workout, - exerciseName: assignment.exerciseName, + exerciseName: exercise.name, date: Date(), - order: assignment.order, - sets: assignment.sets, - reps: assignment.reps, - weight: assignment.weight + order: exercise.order, + sets: exercise.sets, + reps: exercise.reps, + weight: exercise.weight ) modelContext.insert(workoutLog) } diff --git a/Workouts/WorkoutsApp.swift b/Workouts/WorkoutsApp.swift index 724e8f9..0b589c2 100644 --- a/Workouts/WorkoutsApp.swift +++ b/Workouts/WorkoutsApp.swift @@ -19,7 +19,7 @@ struct WorkoutsApp: App { @State private var cloudKitObserver: NSObjectProtocol? init() { - self.container = WorkoutsContainer.create() + self.container = AppContainer.create() // Set up CloudKit notification observation setupCloudKitObservation() diff --git a/Workouts/Utils/Badge.swift b/Workouts/_ATTIC_/Badge.swift similarity index 100% rename from Workouts/Utils/Badge.swift rename to Workouts/_ATTIC_/Badge.swift diff --git a/Workouts/Utils/BadgeView.swift b/Workouts/_ATTIC_/BadgeView.swift similarity index 100% rename from Workouts/Utils/BadgeView.swift rename to Workouts/_ATTIC_/BadgeView.swift diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/_ATTIC_/SettingsView.swift similarity index 97% rename from Workouts/Views/Settings/SettingsView.swift rename to Workouts/_ATTIC_/SettingsView.swift index f9fce6a..68aa098 100644 --- a/Workouts/Views/Settings/SettingsView.swift +++ b/Workouts/_ATTIC_/SettingsView.swift @@ -77,7 +77,7 @@ struct SettingsView: View { private func clearAllData () { do { try deleteAllObjects(ofType: Split.self, from: modelContext) - try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext) + try deleteAllObjects(ofType: Exercise.self, from: modelContext) try deleteAllObjects(ofType: Workout.self, from: modelContext) try deleteAllObjects(ofType: WorkoutLog.self, from: modelContext) try modelContext.save()