This commit is contained in:
2025-07-18 10:03:58 -04:00
parent 4f01a6445f
commit 66f257609f
37 changed files with 845 additions and 704 deletions

View File

@ -1,72 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Exercise {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify, inverse: \ExerciseType.exercises)
var type: ExerciseType?
@Relationship(deleteRule: .nullify, inverse: \Muscle.exercises)
var muscles: [Muscle]? = []
@Relationship(deleteRule: .nullify, inverse: \SplitExerciseAssignment.exercise)
var splits: [SplitExerciseAssignment]? = []
@Relationship(deleteRule: .nullify, inverse: \WorkoutLog.exercise)
var logs: [WorkoutLog]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
static let unnamed = "Unnamed Exercise"
}
extension Exercise: EditableEntity {
static func createNew() -> Exercise {
return Exercise(name: "", descr: "")
}
static var navigationTitle: String {
return "Exercises"
}
@ViewBuilder
static func formView(for model: Exercise) -> some View {
EntityAddEditView(model: model) { $model in
// This internal view is necessary to use @Query within the form.
ExerciseFormView(model: $model)
}
}
}
fileprivate struct ExerciseFormView: View {
@Binding var model: Exercise
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
var body: some View {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Exercise Type")) {
Picker("Type", selection: $model.type) {
Text("Select a type").tag(nil as ExerciseType?)
ForEach(exerciseTypes) { type in
Text(type.name).tag(type as ExerciseType?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}

View File

@ -0,0 +1,66 @@
import Foundation
import Yams
struct ExerciseList: Codable {
let name: String
let source: String
let exercises: [ExerciseItem]
struct ExerciseItem: Codable, Identifiable {
let name: String
let descr: String
let type: String
var id: String { name }
}
}
class ExerciseListLoader {
static func loadExerciseLists() -> [String: ExerciseList] {
var exerciseLists: [String: ExerciseList] = [:]
guard let resourcePath = Bundle.main.resourcePath else {
print("Could not find resource path")
return exerciseLists
}
do {
let fileManager = FileManager.default
let resourceURL = URL(fileURLWithPath: resourcePath)
let yamlFiles = try fileManager.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "yaml" && $0.lastPathComponent.hasSuffix(".exercises.yaml") }
for yamlFile in yamlFiles {
let fileName = yamlFile.lastPathComponent
do {
let yamlString = try String(contentsOf: yamlFile, encoding: .utf8)
if let exerciseList = try Yams.load(yaml: yamlString) as? [String: Any],
let name = exerciseList["name"] as? String,
let source = exerciseList["source"] as? String,
let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
var exercises: [ExerciseList.ExerciseItem] = []
for exerciseData in exercisesData {
if let name = exerciseData["name"] as? String,
let descr = exerciseData["descr"] as? String,
let type = exerciseData["type"] as? String {
let exercise = ExerciseList.ExerciseItem(name: name, descr: descr, type: type)
exercises.append(exercise)
}
}
let exerciseList = ExerciseList(name: name, source: source, exercises: exercises)
exerciseLists[fileName] = exerciseList
}
} catch {
print("Error loading YAML file \(fileName): \(error)")
}
}
} catch {
print("Error listing directory contents: \(error)")
}
return exerciseLists
}
}

View File

@ -1,48 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class ExerciseType {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
var exercises: [Exercise]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
}
// MARK: - EditableEntity Conformance
extension ExerciseType: EditableEntity {
var count: Int? {
return self.exercises?.count
}
static func createNew() -> ExerciseType {
return ExerciseType(name: "", descr: "")
}
static var navigationTitle: String {
return "Exercise Types"
}
@ViewBuilder
static func formView(for model: ExerciseType) -> some View {
EntityAddEditView(model: model) { $model in
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}
}

View File

@ -1,73 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Muscle {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify, inverse: \MuscleGroup.muscles)
var muscleGroup: MuscleGroup?
@Relationship(deleteRule: .nullify)
var exercises: [Exercise]? = []
init(name: String, descr: String, muscleGroup: MuscleGroup? = nil) {
self.name = name
self.descr = descr
self.muscleGroup = muscleGroup
}
}
// MARK: - EditableEntity Conformance
extension Muscle: EditableEntity {
var count: Int? {
return self.exercises?.count
}
static func createNew() -> Muscle {
return Muscle(name: "", descr: "")
}
static var navigationTitle: String {
return "Muscles"
}
@ViewBuilder
static func formView(for model: Muscle) -> some View {
EntityAddEditView(model: model) { $model in
MuscleFormView(model: $model)
}
}
}
// MARK: - Private Form View
fileprivate struct MuscleFormView: View {
@Binding var model: Muscle
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
var body: some View {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Muscle Group")) {
Picker("Muscle Group", selection: $model.muscleGroup) {
Text("Select a muscle group").tag(nil as MuscleGroup?)
ForEach(muscleGroups) { group in
Text(group.name).tag(group as MuscleGroup?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}

View File

@ -1,49 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class MuscleGroup {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
var muscles: [Muscle]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
}
// MARK: - EditableEntity Conformance
extension MuscleGroup: EditableEntity {
static func createNew() -> MuscleGroup {
return MuscleGroup(name: "", descr: "")
}
static var navigationTitle: String {
return "Muscle Groups"
}
@ViewBuilder
static func formView(for model: MuscleGroup) -> some View {
EntityAddEditView(model: model) { $model in
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}
var count: Int? {
return muscles?.count
}
}

View File

@ -5,27 +5,13 @@ import SwiftUI
@Model
final class Split {
var name: String = ""
var intro: String = ""
var color: String = "indigo"
var systemImage: String = "dumbbell.fill"
var order: Int = 0
// Returns the SwiftUI Color for the stored color name
func getColor() -> Color {
switch color {
case "red": return .red
case "orange": return .orange
case "yellow": return .yellow
case "green": return .green
case "mint": return .mint
case "teal": return .teal
case "cyan": return .cyan
case "blue": return .blue
case "indigo": return .indigo
case "purple": return .purple
case "pink": return .pink
case "brown": return .brown
default: return .indigo
}
func getColor () -> Color {
return Color.color(from: self.color)
}
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
@ -34,11 +20,11 @@ final class Split {
@Relationship(deleteRule: .nullify, inverse: \Workout.split)
var workouts: [Workout]? = []
init(name: String, intro: String, color: String = "indigo", systemImage: String = "dumbbell.fill") {
init(name: String, color: String = "indigo", systemImage: String = "dumbbell.fill", order: Int = 0) {
self.name = name
self.intro = intro
self.color = color
self.systemImage = systemImage
self.order = order
}
static let unnamed = "Unnamed Split"
@ -52,7 +38,7 @@ extension Split: EditableEntity {
}
static func createNew() -> Split {
return Split(name: "", intro: "")
return Split(name: "")
}
static var navigationTitle: String {
@ -72,10 +58,6 @@ extension Split: EditableEntity {
fileprivate struct SplitFormView: View {
@Binding var model: Split
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: SplitExerciseAssignment? = nil
@State private var itemToDelete: SplitExerciseAssignment? = nil
// Available colors for splits
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
@ -88,15 +70,10 @@ fileprivate struct SplitFormView: View {
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.intro)
.frame(minHeight: 100)
}
Section(header: Text("Appearance")) {
Picker("Color", selection: $model.color) {
ForEach(availableColors, id: \.self) { colorName in
let tempSplit = Split(name: "", intro: "", color: colorName)
let tempSplit = Split(name: "", color: colorName)
HStack {
Circle()
.fill(tempSplit.getColor())
@ -121,75 +98,7 @@ fileprivate struct SplitFormView: View {
Section(header: Text("Exercises")) {
NavigationLink {
NavigationStack {
Form {
List {
if let assignments = model.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exercise?.name ?? Exercise.unnamed < $1.exercise?.name ?? Exercise.unnamed : $0.order < $1.order })
ForEach(sortedAssignments) { item in
ListItem(
title: item.exercise?.name ?? Exercise.unnamed,
text: item.setup.isEmpty ? nil : item.setup,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
)
.swipeActions {
Button(role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
} else {
Text("No exercises added yet.")
}
}
}
.navigationTitle("Exercises")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView { exercise in
itemToEdit = SplitExerciseAssignment(
order: 0,
sets: 3,
reps: 10,
weight: 40,
split: model,
exercise: exercise
)
}
}
.sheet(item: $itemToEdit) { item in
SplitExerciseAssignmentAddEditView(model: item)
}
.confirmationDialog(
"Delete Exercise?",
isPresented: .constant(itemToDelete != nil),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
model.exercises?.removeAll { $0.id == item.id }
itemToDelete = nil
}
}
}
}
SplitExercisesListView(model: model)
} label: {
ListItem(
text: "Exercises",

View File

@ -3,25 +3,21 @@ import SwiftData
@Model
final class SplitExerciseAssignment {
var exerciseName: String = ""
var order: Int = 0
var sets: Int = 0
var reps: Int = 0
var weight: Int = 0
var setup: String = ""
@Relationship(deleteRule: .nullify)
var split: Split?
@Relationship(deleteRule: .nullify)
var exercise: Exercise?
init(order: Int, sets: Int, reps: Int, weight: Int, setup: String = "", split: Split, exercise: Exercise) {
init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
self.split = split
self.exerciseName = exerciseName
self.order = order
self.sets = sets
self.reps = reps
self.weight = weight
self.setup = setup
self.split = split
self.exercise = exercise
}
}

View File

@ -9,16 +9,14 @@ final class WorkoutLog {
var weight: Int = 0
var status: WorkoutStatus? = WorkoutStatus.notStarted
var order: Int = 0
var exerciseName: String = ""
var completed: Bool = false
@Relationship(deleteRule: .nullify)
var workout: Workout?
@Relationship(deleteRule: .nullify)
var exercise: Exercise?
init(workout: Workout, exercise: Exercise, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
init(workout: Workout, exerciseName: String, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
self.date = date
self.order = order
self.sets = sets
@ -26,7 +24,7 @@ final class WorkoutLog {
self.weight = weight
self.status = status
self.workout = workout
self.exercise = exercise
self.exerciseName = exerciseName
self.completed = completed
}
}