wip
This commit is contained in:
@ -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;
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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)")
|
@ -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?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
@ -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
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
50
Workouts/Utils/Date+humanTimeInterval.swift
Normal file
50
Workouts/Utils/Date+humanTimeInterval.swift
Normal file
@ -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..<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")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
@ -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)
|
@ -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") {
|
@ -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 {
|
153
Workouts/Views/Exercises/ExercisePickerView.swift
Normal file
153
Workouts/Views/Exercises/ExercisePickerView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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?",
|
@ -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
|
||||||
}
|
}
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
@ -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)")
|
@ -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
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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()
|
||||||
|
@ -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()
|
Reference in New Issue
Block a user