wip
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
66
Workouts/Models/ExerciseList.swift
Normal file
66
Workouts/Models/ExerciseList.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user