This commit is contained in:
2025-07-19 16:42:47 -04:00
parent 6e46775f58
commit e3c3f2c6f0
38 changed files with 556 additions and 367 deletions

View File

@ -347,6 +347,7 @@
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_SOURCE_FILE_NAMES = "_ATTIC_/*";
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -410,6 +411,7 @@
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_SOURCE_FILE_NAMES = "_ATTIC_/*";
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

View File

@ -21,7 +21,7 @@ struct ContentView: View {
Label("Workouts", systemImage: "figure.strengthtraining.traditional") Label("Workouts", systemImage: "figure.strengthtraining.traditional")
} }
WorkoutsView() WorkoutListView()
.tabItem { .tabItem {
Label("Logs", systemImage: "list.bullet.clipboard.fill") Label("Logs", systemImage: "list.bullet.clipboard.fill")
} }
@ -36,10 +36,10 @@ struct ContentView: View {
Label("Reports", systemImage: "chart.bar") Label("Reports", systemImage: "chart.bar")
} }
SettingsView() // SettingsView()
.tabItem { // .tabItem {
Label("Settings", systemImage: "gear") // Label("Settings", systemImage: "gear")
} // }
} }
.observeCloudKitChanges() .observeCloudKitChanges()
} }

View File

@ -2,8 +2,8 @@ import Foundation
import SwiftData import SwiftData
@Model @Model
final class SplitExerciseAssignment { final class Exercise {
var exerciseName: String = "" var name: String = ""
var order: Int = 0 var order: Int = 0
var sets: Int = 0 var sets: Int = 0
var reps: 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) { init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
self.split = split self.split = split
self.exerciseName = exerciseName self.name = exerciseName
self.order = order self.order = order
self.sets = sets self.sets = sets
self.reps = reps self.reps = reps

View File

@ -15,8 +15,8 @@ final class Split {
return Color.color(from: self.color) return Color.color(from: self.color)
} }
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split) @Relationship(deleteRule: .cascade, inverse: \Exercise.split)
var exercises: [SplitExerciseAssignment]? = [] var exercises: [Exercise]? = []
@Relationship(deleteRule: .nullify, inverse: \Workout.split) @Relationship(deleteRule: .nullify, inverse: \Workout.split)
var workouts: [Workout]? = [] var workouts: [Workout]? = []
@ -109,7 +109,7 @@ fileprivate struct SplitFormView: View {
Section(header: Text("Exercises")) { Section(header: Text("Exercises")) {
NavigationLink { NavigationLink {
SplitExercisesListView(model: model) ExerciseListView(split: model)
} label: { } label: {
ListItem( ListItem(
text: "Exercises", text: "Exercises",

View File

@ -5,6 +5,7 @@ import SwiftData
final class Workout { final class Workout {
var start: Date = Date() var start: Date = Date()
var end: Date? var end: Date?
var status: WorkoutStatus? = WorkoutStatus.notStarted
@Relationship(deleteRule: .nullify) @Relationship(deleteRule: .nullify)
var split: Split? var split: Split?
@ -12,13 +13,17 @@ final class Workout {
@Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout) @Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout)
var logs: [WorkoutLog]? = [] var logs: [WorkoutLog]? = []
init(start: Date, end: Date? = nil, split: Split?) { init(start: Date, end: Date, split: Split?) {
self.start = start self.start = start
self.end = end self.end = end
self.split = split self.split = split
} }
var label: String { var label: String {
start.formattedDate() if status == .completed, let endDate = end {
return "\(start.formattedDate())\(endDate.formattedDate())"
} else {
return start.formattedDate()
}
} }
} }

View File

@ -1,12 +0,0 @@
//
// ListableItem.swift
// Workouts
//
// Created by rzen on 7/13/25 at 10:40AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
protocol ListableItem {
var name: String { get set }
}

View File

@ -6,64 +6,78 @@ exercises:
shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades
together. Avoid leaning back excessively or using momentum. together. Avoid leaning back excessively or using momentum.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Seated Row - name: Seated Row
descr: With your chest firmly against the pad, grip the handles and pull straight 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 back while keeping your elbows close to your body. Focus on retracting your shoulder
blades and avoid rounding your back. blades and avoid rounding your back.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Shoulder Press - name: Shoulder Press
descr: Sit with your back against the pad, grip the handles just outside shoulder-width. 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 Press upward without locking out your elbows. Keep your neck relaxed and avoid
shrugging your shoulders. shrugging your shoulders.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Chest Press - name: Chest Press
descr: Adjust the seat so the handles are at mid-chest height. Push forward until arms 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 are nearly extended, then return slowly. Keep wrists straight and dont let your elbows
drop too low. drop too low.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Tricep Press - name: Tricep Press
descr: With elbows close to your sides, press the handles downward in a controlled 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. motion. Avoid flaring your elbows or using your shoulders to assist the motion.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Arm Curl - name: Arm Curl
descr: Position your arms over the pad and grip the handles. Curl the weight upward 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 while keeping your upper arms stationary. Avoid using momentum and fully control
the lowering phase. the lowering phase.
type: Machine-Based type: Machine-Based
split: Upper Body
- name: Abdominal - name: Abdominal
descr: Sit with the pads resting against your chest. Contract your abs to curl forward, 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 keeping your lower back in contact with the pad. Avoid pulling with your arms
or hips. or hips.
type: Machine-Based type: Machine-Based
split: Core
- name: Rotary - name: Rotary
descr: Rotate your torso from side to side in a controlled motion, keeping your 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 hips still. Focus on using your obliques to generate the twist, not momentum or
the arms. the arms.
type: Machine-Based type: Machine-Based
split: Core
- name: Leg Press - name: Leg Press
descr: Place your feet shoulder-width on the platform. Press upward through your 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 heels without locking your knees. Keep your back flat against the pad throughout
the motion. the motion.
type: Machine-Based type: Machine-Based
split: Lower Body
- name: Leg Extension - name: Leg Extension
descr: Sit upright and align your knees with the pivot point. Extend your legs to 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 a straightened position, then lower with control. Avoid jerky movements or lifting
your hips off the seat. your hips off the seat.
type: Machine-Based type: Machine-Based
split: Lower Body
- name: Leg Curl - name: Leg Curl
descr: Lie face down or sit depending on the version. Curl your legs toward your 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. glutes, focusing on hamstring engagement. Avoid arching your back or using momentum.
type: Machine-Based type: Machine-Based
split: Lower Body
- name: Adductor - name: Adductor
descr: Sit with legs placed outside the pads. Bring your legs together using inner 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. thigh muscles. Control the motion both in and out, avoiding fast swings.
type: Machine-Based type: Machine-Based
split: Lower Body
- name: Abductor - name: Abductor
descr: Sit with legs inside the pads and push outward to engage outer thighs and 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. glutes. Avoid leaning forward and keep the motion controlled throughout.
type: Machine-Based type: Machine-Based
split: Lower Body
- name: Calfs - name: Calfs
descr: Place the balls of your feet on the platform with heels hanging off. Raise 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 your heels by contracting your calves, then slowly lower them below the platform
level for a full stretch. level for a full stretch.
type: Machine-Based type: Machine-Based
split: Lower Body

View File

@ -1,15 +1,15 @@
import Foundation import Foundation
import SwiftData import SwiftData
final class WorkoutsContainer { final class AppContainer {
static let logger = AppLogger( static let logger = AppLogger(
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
category: "WorkoutsContainer" category: "AppContainer"
) )
static func create() -> ModelContainer { static func create() -> ModelContainer {
// Using the current models directly without migration plan to avoid reference errors // 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 configuration = ModelConfiguration(cloudKitDatabase: .automatic)
let container = try! ModelContainer(for: schema, configurations: configuration) let container = try! ModelContainer(for: schema, configurations: configuration)
return container return container
@ -20,10 +20,8 @@ final class WorkoutsContainer {
let configuration = ModelConfiguration(isStoredInMemoryOnly: true) let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
do { do {
let schema = Schema(SchemaV1.models) let schema = Schema(SchemaVersion.models)
let container = try ModelContainer(for: schema, configurations: configuration) let container = try ModelContainer(for: schema, configurations: configuration)
let context = ModelContext(container)
return container return container
} catch { } catch {
fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)") fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")

View File

@ -12,23 +12,14 @@ struct CloudKitSyncObserver: ViewModifier {
.onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in .onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in
refreshID = UUID() refreshID = UUID()
Task { @MainActor in Task { @MainActor in
// do { do {
// for entity in modelContext.container.schema.entities { let _ = try await fetchAll(of: Exercise.self, from: modelContext)
// fetchAll<entity.Type>(of: entity.Type, from: modelContext) let _ = try await fetchAll(of: Split.self, from: modelContext)
// } let _ = try await fetchAll(of: Workout.self, from: modelContext)
// } catch { let _ = try await fetchAll(of: WorkoutLog.self, from: modelContext)
// print("ERROR: failed to fetch data on CloudKit change") } catch {
// } print("ERROR: failed to fetch \(error.localizedDescription)")
// }
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<ExerciseType>())
// try? modelContext.fetch(FetchDescriptor<Muscle>())
// try? modelContext.fetch(FetchDescriptor<MuscleGroup>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// TODO: add more entities?
} }
} }
} }

View File

@ -5,7 +5,7 @@ enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] = [ static var models: [any PersistentModel.Type] = [
Split.self, Split.self,
SplitExerciseAssignment.self, Exercise.self,
Workout.self, Workout.self,
WorkoutLog.self WorkoutLog.self
] ]

View File

@ -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
]
}

View File

@ -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
]
}

View File

@ -2,8 +2,18 @@ import SwiftData
enum SchemaVersion: Int { enum SchemaVersion: Int {
case v1 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
}
}
} }

View File

@ -1,24 +1,29 @@
import SwiftData import SwiftData
import Foundation
struct WorkoutsMigrationPlan: SchemaMigrationPlan { struct WorkoutsMigrationPlan: SchemaMigrationPlan {
static var schemas: [VersionedSchema.Type] = [ static var schemas: [VersionedSchema.Type] = SchemaVersion.schemas
SchemaV1.self,
SchemaV2.self
]
static var stages: [MigrationStage] = [ static var stages: [MigrationStage] = [
// Migration from V1 to V2: Add status field to WorkoutLog
MigrationStage.custom( MigrationStage.custom(
fromVersion: SchemaV1.self, fromVersion: SchemaV1.self,
toVersion: SchemaV2.self, toVersion: SchemaV1.self,
willMigrate: { context in willMigrate: { context in
// Get all WorkoutLog instances print("migrating from v1 to v1")
let workoutLogs = try? context.fetch(FetchDescriptor<WorkoutLog>()) let workouts = try? context.fetch(FetchDescriptor<Workout>())
workouts?.forEach { workout in
if let status = workout.status {
// Update each WorkoutLog with appropriate status based on completed flag } else {
workoutLogs?.forEach { workoutLog in workout.status = .notStarted
// If completed is true, set status to .completed, otherwise set to .notStarted }
workoutLog.status = workoutLog.completed ? WorkoutStatus.completed : WorkoutStatus.notStarted
// if let endDate = workout.end {
//
// } else {
// workout.end = Date()
// }
workout.end = Date()
} }
}, },
didMigrate: { _ in didMigrate: { _ in

View File

@ -0,0 +1,50 @@
//
// Date+humanTimeInterval.swift
// Workouts
//
// Created by rzen on 7/19/25 at 1:06PM.
//
// 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..<minute:
return "\(absSeconds) seconds"
case minute..<hour:
let minutes = absSeconds / minute
return "\(minutes) minute\(minutes == 1 ? "" : "s")"
case hour..<day:
let hours = absSeconds / hour
return "\(hours) hour\(hours == 1 ? "" : "s")"
case day..<week:
let days = absSeconds / day
return "\(days) day\(days == 1 ? "" : "s")"
case week..<month:
let weeks = absSeconds / week
return "\(weeks) week\(weeks == 1 ? "" : "s")"
case month..<year:
let months = absSeconds / month
return "\(months) month\(months == 1 ? "" : "s")"
default:
let years = absSeconds / year
return "\(years) year\(years == 1 ? "" : "s")"
}
}
}

View File

@ -13,6 +13,7 @@ struct CalendarListItem: View {
var date: Date var date: Date
var title: String var title: String
var subtitle: String? var subtitle: String?
var subtitle2: String?
var count: Int? var count: Int?
var body: some View { var body: some View {
@ -31,7 +32,7 @@ struct CalendarListItem: View {
} }
.padding([.trailing], 10) .padding([.trailing], 10)
} }
HStack { HStack (alignment: .top) {
VStack (alignment: .leading) { VStack (alignment: .leading) {
Text("\(title)") Text("\(title)")
.font(.headline) .font(.headline)
@ -39,6 +40,10 @@ struct CalendarListItem: View {
Text("\(subtitle)") Text("\(subtitle)")
.font(.footnote) .font(.footnote)
} }
if let subtitle = subtitle2 {
Text("\(subtitle)")
.font(.footnote)
}
} }
if let count = count { if let count = count {
Spacer() Spacer()

View File

@ -14,7 +14,7 @@ struct ListItem: View {
var text: String? var text: String?
var subtitle: String? var subtitle: String?
var count: Int? var count: Int?
var badges: [Badge]? = [] // var badges: [Badge]? = []
var body: some View { var body: some View {
HStack { HStack {
@ -32,11 +32,11 @@ struct ListItem: View {
} }
} }
HStack (alignment: .bottom) { HStack (alignment: .bottom) {
if let badges = badges { // if let badges = badges {
ForEach (badges, id: \.self) { badge in // ForEach (badges, id: \.self) { badge in
BadgeView(badge: badge) // BadgeView(badge: badge)
} // }
} // }
if let subtitle = subtitle { if let subtitle = subtitle {
Text("\(subtitle)") Text("\(subtitle)")
.font(.footnote) .font(.footnote)

View File

@ -9,12 +9,12 @@
import SwiftUI import SwiftUI
struct SplitExerciseAssignmentAddEditView: View { struct ExerciseAddEditView: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var showingExercisePicker = false @State private var showingExercisePicker = false
@State var model: SplitExerciseAssignment @State var model: Exercise
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@ -24,7 +24,7 @@ struct SplitExerciseAssignmentAddEditView: View {
showingExercisePicker = true showingExercisePicker = true
}) { }) {
HStack { HStack {
Text(model.exerciseName.isEmpty ? "Select Exercise" : model.exerciseName) Text(model.name.isEmpty ? "Select Exercise" : model.name)
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.foregroundColor(.gray) .foregroundColor(.gray)
@ -54,11 +54,11 @@ struct SplitExerciseAssignmentAddEditView: View {
} }
} }
.sheet(isPresented: $showingExercisePicker) { .sheet(isPresented: $showingExercisePicker) {
ExercisePickerView { exerciseName in ExercisePickerView { exerciseNames in
model.exerciseName = exerciseName model.name = exerciseNames.first ?? "Exercise.unnamed"
} }
} }
.navigationTitle(model.exerciseName.isEmpty ? "New Exercise" : model.exerciseName) .navigationTitle(model.name.isEmpty ? "New Exercise" : model.name)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button("Cancel") {

View File

@ -1,23 +1,24 @@
import Foundation import Foundation
import Yams import Yams
struct ExerciseList: Codable {
let name: String
let source: String
let exercises: [ExerciseItem]
struct ExerciseItem: Codable, Identifiable {
let name: String
let descr: String
let type: String
var id: String { name }
}
}
class ExerciseListLoader { class ExerciseListLoader {
static func loadExerciseLists() -> [String: ExerciseList] { struct ExerciseListData: Codable {
var exerciseLists: [String: ExerciseList] = [:] 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 { guard let resourcePath = Bundle.main.resourcePath else {
print("Could not find resource path") print("Could not find resource path")
@ -39,18 +40,19 @@ class ExerciseListLoader {
let source = exerciseList["source"] as? String, let source = exerciseList["source"] as? String,
let exercisesData = exerciseList["exercises"] as? [[String: Any]] { let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
var exercises: [ExerciseList.ExerciseItem] = [] var exercises: [ExerciseListData.ExerciseItem] = []
for exerciseData in exercisesData { for exerciseData in exercisesData {
if let name = exerciseData["name"] as? String, if let name = exerciseData["name"] as? String,
let descr = exerciseData["descr"] as? String, let descr = exerciseData["descr"] as? String,
let type = exerciseData["type"] as? String { let type = exerciseData["type"] as? String,
let exercise = ExerciseList.ExerciseItem(name: name, descr: descr, type: type) let split = exerciseData["split"] as? String {
let exercise = ExerciseListData.ExerciseItem(name: name, descr: descr, type: type, split: split)
exercises.append(exercise) exercises.append(exercise)
} }
} }
let exerciseList = ExerciseList(name: name, source: source, exercises: exercises) let exerciseList = ExerciseListData(name: name, source: source, exercises: exercises)
exerciseLists[fileName] = exerciseList exerciseLists[fileName] = exerciseList
} }
} catch { } catch {

View File

@ -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<String> = []
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()
}
}

View File

@ -10,35 +10,51 @@
import SwiftUI import SwiftUI
import SwiftData import SwiftData
struct SplitExercisesListView: View { struct ExerciseListView: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss @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 showingAddSheet: Bool = false
@State private var itemToEdit: SplitExerciseAssignment? = nil @State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: SplitExerciseAssignment? = nil @State private var itemToDelete: Exercise? = nil
@State private var createdWorkout: Workout? = 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<Split> { s in
s.persistentModelID == splitId
})
}
var body: some View { 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 { NavigationStack {
Form { Form {
List { List {
if let assignments = model.exercises, !assignments.isEmpty { if let assignments = currentSplit.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order }) let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.name < $1.name : $0.order < $1.order })
ForEach(sortedAssignments) { item in ForEach(sortedAssignments) { item in
ListItem( ListItem(
title: item.exerciseName, title: item.name,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)" subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
) )
.swipeActions { .swipeActions {
Button { Button {
itemToDelete = item itemToDelete = item
} label: { } label: {
Label("Delete", systemImage: "circle") Label("Delete", systemImage: "trash")
} }
.tint(.red)
Button { Button {
itemToEdit = item itemToEdit = item
} label: { } label: {
@ -76,19 +92,19 @@ struct SplitExercisesListView: View {
} }
} }
} }
.navigationTitle("\(model.name)") .navigationTitle("\(currentSplit.name)")
} }
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button("Start This Split") { Button("Start This Split") {
let split = model let split = currentSplit
let workout = Workout(start: Date(), split: split) let workout = Workout(start: Date(), end: Date(), split: split)
modelContext.insert(workout) modelContext.insert(workout)
if let exercises = split.exercises { if let exercises = split.exercises {
for assignment in exercises { for assignment in exercises {
let workoutLog = WorkoutLog( let workoutLog = WorkoutLog(
workout: workout, workout: workout,
exerciseName: assignment.exerciseName, exerciseName: assignment.name,
date: Date(), date: Date(),
order: assignment.order, order: assignment.order,
sets: assignment.sets, sets: assignment.sets,
@ -106,7 +122,7 @@ struct SplitExercisesListView: View {
} }
} }
.navigationDestination(item: $createdWorkout, destination: { workout in .navigationDestination(item: $createdWorkout, destination: { workout in
WorkoutLogView(workout: workout) WorkoutLogListView(workout: workout)
}) })
// .toolbar { // .toolbar {
// ToolbarItem(placement: .navigationBarTrailing) { // ToolbarItem(placement: .navigationBarTrailing) {
@ -116,19 +132,49 @@ struct SplitExercisesListView: View {
// } // }
// } // }
.sheet (isPresented: $showingAddSheet) { .sheet (isPresented: $showingAddSheet) {
ExercisePickerView { exerciseName in ExercisePickerView(onExerciseSelected: { exerciseNames in
itemToEdit = SplitExerciseAssignment( let splitId = currentSplit.persistentModelID
split: model, print("exerciseNames: \(exerciseNames)")
exerciseName: exerciseName, if exerciseNames.count == 1 {
order: 0, itemToEdit = Exercise(
sets: 3, split: split,
reps: 10, exerciseName: exerciseNames.first ?? "Exercise.unnamed",
weight: 40 order: 0,
) sets: 3,
} reps: 10,
weight: 40
)
} else {
for exerciseName in exerciseNames {
var duplicateExercise: [Exercise]? = nil
do {
duplicateExercise = try modelContext.fetch(FetchDescriptor<Exercise>(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 .sheet(item: $itemToEdit) { item in
SplitExerciseAssignmentAddEditView(model: item) ExerciseAddEditView(model: item)
} }
.confirmationDialog( .confirmationDialog(
"Delete Exercise?", "Delete Exercise?",

View File

@ -23,7 +23,7 @@ extension Split: OrderableItem {
} }
/// Extension to make SplitExerciseAssignment conform to OrderableItem /// Extension to make SplitExerciseAssignment conform to OrderableItem
extension SplitExerciseAssignment: OrderableItem { extension Exercise: OrderableItem {
func updateOrder(to index: Int) { func updateOrder(to index: Int) {
self.order = index self.order = index
} }

View File

@ -10,6 +10,9 @@
import SwiftUI import SwiftUI
struct SplitAddEditView: View { struct SplitAddEditView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State var model: Split @State var model: Split
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"] 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"] 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 { var body: some View {
Form { NavigationStack {
Section(header: Text("Name")) { Form {
TextField("Name", text: $model.name) Section(header: Text("Name")) {
.bold() TextField("Name", text: $model.name)
} .bold()
}
Section(header: Text("Appearance")) { Section(header: Text("Appearance")) {
Picker("Color", selection: $model.color) { Picker("Color", selection: $model.color) {
ForEach(availableColors, id: \.self) { colorName in ForEach(availableColors, id: \.self) { colorName in
let tempSplit = Split(name: "", color: colorName) let tempSplit = Split(name: "", color: colorName)
HStack { HStack {
Circle() Circle()
.fill(tempSplit.getColor()) .fill(tempSplit.getColor())
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text(colorName.capitalized) 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) { Section(header: Text("Exercises")) {
ForEach(availableIcons, id: \.self) { iconName in NavigationLink {
HStack { ExerciseListView(split: model)
Image(systemName: iconName) } label: {
.frame(width: 24, height: 24) ListItem(
Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized) text: "Exercises",
} count: model.exercises?.count ?? 0
.tag(iconName) )
} }
} }
} }
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
Section(header: Text("Exercises")) { ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink { Button("Save") {
SplitExercisesListView(model: model) try? modelContext.save()
} label: { dismiss()
ListItem( }
text: "Exercises",
count: model.exercises?.count ?? 0
)
} }
} }
} }

View File

@ -9,7 +9,7 @@
import SwiftUI import SwiftUI
struct DraggableSplitItem: View { struct SplitItem: View {
var name: String var name: String
var color: Color var color: Color
@ -31,15 +31,15 @@ struct DraggableSplitItem: View {
.aspectRatio(1.618, contentMode: .fit) .aspectRatio(1.618, contentMode: .fit)
.shadow(radius: 2) .shadow(radius: 2)
GeometryReader { geometry in GeometryReader { geo in
VStack(spacing: 4) { VStack(spacing: 4) {
Spacer() Spacer()
// Icon in the center - now using dynamic sizing // Icon in the center - now using dynamic sizing
Image(systemName: systemImageName) 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() .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) .padding(.bottom, 4)
// Name at the bottom inside the rectangle // Name at the bottom inside the rectangle
@ -53,7 +53,7 @@ struct DraggableSplitItem: View {
.padding(.bottom, 8) .padding(.bottom, 8)
} }
.foregroundColor(.white) .foregroundColor(.white)
.frame(width: geometry.size.width, height: geometry.size.height) .frame(width: geo.size.width, height: geo.size.height)
} }
} }
} }

View File

@ -25,9 +25,9 @@ struct SplitsView: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink { NavigationLink {
SplitExercisesListView(model: split) ExerciseListView(split: split)
} label: { } label: {
DraggableSplitItem( SplitItem(
name: split.name, name: split.name,
color: Color.color(from: split.color), color: Color.color(from: split.color),
systemImageName: split.systemImage, systemImageName: split.systemImage,
@ -40,18 +40,7 @@ struct SplitsView: View {
.padding() .padding()
} }
.navigationTitle("Splits") .navigationTitle("Splits")
.onAppear { .onAppear(perform: loadSplits)
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
} catch {
print("ERROR: failed to load splits \(error)")
}
}
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) { Button(action: { showingAddSheet.toggle() }) {
@ -65,4 +54,17 @@ struct SplitsView: View {
} }
} }
func loadSplits () {
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
} catch {
print("ERROR: failed to load splits \(error)")
}
}
} }

View File

@ -87,12 +87,12 @@ struct WorkoutLogEditView: View {
// Find the matching exercise in split.exercises by name // Find the matching exercise in split.exercises by name
if let exercises = split?.exercises { if let exercises = split?.exercises {
for exerciseAssignment in exercises { for exercise in exercises {
if exerciseAssignment.exerciseName == workoutLog.exerciseName { if exercise.name == workoutLog.exerciseName {
// Update the sets, reps, and weight in the split exercise assignment // Update the sets, reps, and weight in the split exercise assignment
exerciseAssignment.sets = workoutLog.sets exercise.sets = workoutLog.sets
exerciseAssignment.reps = workoutLog.reps exercise.reps = workoutLog.reps
exerciseAssignment.weight = workoutLog.weight exercise.weight = workoutLog.weight
// Save the changes to the split // Save the changes to the split
try? modelContext.save() try? modelContext.save()

View File

@ -10,7 +10,7 @@
import SwiftUI import SwiftUI
import SwiftData import SwiftData
struct WorkoutLogView: View { struct WorkoutLogListView: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@State var workout: Workout @State var workout: Workout
@ -47,10 +47,7 @@ struct WorkoutLogView: View {
if [.inProgress,.completed].contains(status) { if [.inProgress,.completed].contains(status) {
Button { Button {
withAnimation { resetWorkout(log)
log.status = .notStarted
try? modelContext.save()
}
} label: { } label: {
Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName) Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName)
} }
@ -59,10 +56,7 @@ struct WorkoutLogView: View {
if [.notStarted,.completed].contains(status) { if [.notStarted,.completed].contains(status) {
Button { Button {
withAnimation { startWorkout(log)
log.status = .inProgress
try? modelContext.save()
}
} label: { } label: {
Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName) Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName)
} }
@ -71,10 +65,7 @@ struct WorkoutLogView: View {
if [.notStarted,.inProgress].contains(status) { if [.notStarted,.inProgress].contains(status) {
Button { Button {
withAnimation { completeWorkout(log)
log.status = .completed
try? modelContext.save()
}
} label: { } label: {
Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName) Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName)
} }
@ -110,11 +101,11 @@ struct WorkoutLogView: View {
} }
} }
.sheet(isPresented: $showingAddSheet) { .sheet(isPresented: $showingAddSheet) {
ExercisePickerView { exerciseName in ExercisePickerView { exerciseNames in
let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext) let setsRepsWeight = getSetsRepsWeight(exerciseNames.first ?? "Exercise.unnamed", in: modelContext)
let workoutLog = WorkoutLog( let workoutLog = WorkoutLog(
workout: workout, workout: workout,
exerciseName: exerciseName, exerciseName: exerciseNames.first ?? "Exercise.unnamed",
date: Date(), date: Date(),
sets: setsRepsWeight.sets, sets: setsRepsWeight.sets,
reps: setsRepsWeight.reps, 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 { func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
// Use a single expression predicate that works with SwiftData // Use a single expression predicate that works with SwiftData
print("Searching for exercise name: \(exerciseName)") print("Searching for exercise name: \(exerciseName)")

View File

@ -13,6 +13,17 @@ enum WorkoutStatus: Int, Codable {
case completed = 3 case completed = 3
case skipped = 4 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 { var checkboxStatus: CheckboxStatus {
switch (self) { switch (self) {
case .notStarted: .unchecked case .notStarted: .unchecked

View File

@ -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()
}
}

View File

@ -20,44 +20,20 @@ struct WorkoutEditView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { Form {
// Section (header: Text("Split")) { Section (header: Text("Split")) {
// Text("\(workout.split?.name ?? Split.unnamed)") Text("\(workout.split?.name ?? Split.unnamed)")
// } }
Section (header: Text("Status")) {
Text("\(workout.status?.name ?? WorkoutStatus.unnamed)")
}
Section (header: Text("Start/End")) { Section (header: Text("Start/End")) {
DatePicker("Started", selection: $workout.start) DatePicker("Started", selection: $workout.start)
Toggle("Workout Ended", isOn: Binding( if workout.status == .completed {
get: { workout.end != nil },
set: { newValue in
withAnimation {
if newValue {
workoutEndDate = Date()
workout.end = workoutEndDate
} else {
workout.end = nil
}
}
}
))
if workout.end != nil {
DatePicker("Ended", selection: $workoutEndDate) 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") .navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
.toolbar { .toolbar {
@ -70,6 +46,9 @@ struct WorkoutEditView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { Button("Save") {
try? modelContext.save() try? modelContext.save()
if workout.status == .completed {
workout.end = workoutEndDate
}
dismiss() dismiss()
} }
} }

View File

@ -10,7 +10,7 @@
import SwiftUI import SwiftUI
import SwiftData import SwiftData
struct WorkoutsView: View { struct WorkoutListView: View {
private let logger = AppLogger( private let logger = AppLogger(
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts", subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
category: "WorkoutsView" category: "WorkoutsView"
@ -33,11 +33,12 @@ struct WorkoutsView: View {
} else { } else {
List { List {
ForEach (workouts) { workout in ForEach (workouts) { workout in
NavigationLink(destination: WorkoutLogView(workout: workout)) { NavigationLink(destination: WorkoutLogListView(workout: workout)) {
CalendarListItem( CalendarListItem(
date: workout.start, date: workout.start,
title: workout.split?.name ?? Split.unnamed, 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) { .swipeActions(edge: .trailing, allowsFullSwipe: false) {
@ -59,16 +60,9 @@ struct WorkoutsView: View {
} }
} }
.navigationTitle("Workouts") .navigationTitle("Workouts")
// .toolbar { .sheet(item: $itemToEdit) { item in
// ToolbarItem(placement: .primaryAction) { WorkoutEditView(workout: item)
// Button("Start Workout") { }
// showingSplitPicker = true
// }
// }
// }
// .sheet(item: $itemToEdit) { item in
// WorkoutEditView(workout: item)
// }
.confirmationDialog( .confirmationDialog(
"Delete?", "Delete?",
isPresented: Binding<Bool>( isPresented: Binding<Bool>(
@ -94,18 +88,18 @@ struct WorkoutsView: View {
} }
.sheet(isPresented: $showingSplitPicker) { .sheet(isPresented: $showingSplitPicker) {
SplitPickerView { split in SplitPickerView { split in
let workout = Workout(start: Date(), split: split) let workout = Workout(start: Date(), end: Date(), split: split)
modelContext.insert(workout) modelContext.insert(workout)
if let exercises = split.exercises { if let exercises = split.exercises {
for assignment in exercises { for exercise in exercises {
let workoutLog = WorkoutLog( let workoutLog = WorkoutLog(
workout: workout, workout: workout,
exerciseName: assignment.exerciseName, exerciseName: exercise.name,
date: Date(), date: Date(),
order: assignment.order, order: exercise.order,
sets: assignment.sets, sets: exercise.sets,
reps: assignment.reps, reps: exercise.reps,
weight: assignment.weight weight: exercise.weight
) )
modelContext.insert(workoutLog) modelContext.insert(workoutLog)
} }

View File

@ -19,7 +19,7 @@ struct WorkoutsApp: App {
@State private var cloudKitObserver: NSObjectProtocol? @State private var cloudKitObserver: NSObjectProtocol?
init() { init() {
self.container = WorkoutsContainer.create() self.container = AppContainer.create()
// Set up CloudKit notification observation // Set up CloudKit notification observation
setupCloudKitObservation() setupCloudKitObservation()

View File

@ -77,7 +77,7 @@ struct SettingsView: View {
private func clearAllData () { private func clearAllData () {
do { do {
try deleteAllObjects(ofType: Split.self, from: modelContext) 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: Workout.self, from: modelContext)
try deleteAllObjects(ofType: WorkoutLog.self, from: modelContext) try deleteAllObjects(ofType: WorkoutLog.self, from: modelContext)
try modelContext.save() try modelContext.save()