wip
This commit is contained in:
27
Workouts/Cepo/EditableEntity.swift
Normal file
27
Workouts/Cepo/EditableEntity.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
/// A protocol for entities that can be managed in a generic list view.
|
||||||
|
protocol EditableEntity: PersistentModel, Identifiable, Hashable {
|
||||||
|
/// The name of the entity to be displayed in the list.
|
||||||
|
var name: String { get set }
|
||||||
|
|
||||||
|
/// A view for adding or editing the entity.
|
||||||
|
associatedtype FormView: View
|
||||||
|
@ViewBuilder static func formView(for model: Self) -> FormView
|
||||||
|
|
||||||
|
/// Creates a new, empty instance of the entity.
|
||||||
|
static func createNew() -> Self
|
||||||
|
|
||||||
|
/// The title for the navigation bar in the list view.
|
||||||
|
static var navigationTitle: String { get }
|
||||||
|
|
||||||
|
/// An optional property to specify a count to be displayed in the list item.
|
||||||
|
var count: Int? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditableEntity {
|
||||||
|
var count: Int? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
48
Workouts/Cepo/EntityAddEditView.swift
Normal file
48
Workouts/Cepo/EntityAddEditView.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct EntityAddEditView<T: EditableEntity, Content: View>: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
|
@State var model: T
|
||||||
|
private let content: (Binding<T>) -> Content
|
||||||
|
|
||||||
|
init(model: T, @ViewBuilder content: @escaping (Binding<T>) -> Content) {
|
||||||
|
_model = State(initialValue: model)
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
content($model)
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Save") {
|
||||||
|
// If the model is not in a context, it's a new one.
|
||||||
|
if model.modelContext == nil {
|
||||||
|
modelContext.insert(model)
|
||||||
|
}
|
||||||
|
// The save is in a do-catch block to handle potential errors,
|
||||||
|
// such as constraint violations.
|
||||||
|
do {
|
||||||
|
try modelContext.save()
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
// In a real app, you'd want to present this error to the user.
|
||||||
|
print("Failed to save model: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,35 @@
|
|||||||
//
|
|
||||||
// MuscleGroupsListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
struct MuscleGroupsListView: View {
|
struct EntityListView<T: EditableEntity>: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var items: [MuscleGroup]
|
@Query var items: [T]
|
||||||
|
|
||||||
@State var showingAddSheet = false
|
@State private var showingAddSheet = false
|
||||||
@State var itemToEdit: MuscleGroup? = nil
|
@State private var itemToEdit: T? = nil
|
||||||
@State var itemToDelete: MuscleGroup? = nil
|
@State private var itemToDelete: T? = nil
|
||||||
|
|
||||||
|
private var sortDescriptors: [SortDescriptor<T>]
|
||||||
|
|
||||||
|
init(sort: [SortDescriptor<T>] = [], searchString: String = "") {
|
||||||
|
self.sortDescriptors = sort
|
||||||
|
_items = Query(filter: #Predicate { item in
|
||||||
|
if searchString.isEmpty {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return item.name.localizedStandardContains(searchString)
|
||||||
|
}
|
||||||
|
}, sort: sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var isInsideNavigationStack: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
let content = Form {
|
||||||
List {
|
List {
|
||||||
ForEach(items) { item in
|
ForEach(items) { item in
|
||||||
ListItem(title: item.name, count: item.muscles?.count)
|
ListItem(title: item.name, count: item.count)
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
itemToDelete = item
|
itemToDelete = item
|
||||||
@ -40,21 +46,19 @@ struct MuscleGroupsListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Muscle Groups")
|
.navigationTitle(T.navigationTitle)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(action: {
|
Button(action: { showingAddSheet.toggle() }) {
|
||||||
showingAddSheet.toggle()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingAddSheet) {
|
.sheet(isPresented: $showingAddSheet) {
|
||||||
MuscleGroupAddEditView()
|
T.formView(for: T.createNew())
|
||||||
}
|
}
|
||||||
.sheet(item: $itemToEdit) { item in
|
.sheet(item: $itemToEdit) { item in
|
||||||
MuscleGroupAddEditView(model: item)
|
T.formView(for: item)
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
"Delete?",
|
"Delete?",
|
||||||
@ -65,10 +69,10 @@ struct MuscleGroupsListView: View {
|
|||||||
titleVisibility: .visible
|
titleVisibility: .visible
|
||||||
) {
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let itemToDelete = itemToDelete {
|
if let item = itemToDelete {
|
||||||
modelContext.delete(itemToDelete)
|
modelContext.delete(item)
|
||||||
try? modelContext.save()
|
try? modelContext.save()
|
||||||
self.itemToDelete = nil
|
itemToDelete = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
@ -77,5 +81,16 @@ struct MuscleGroupsListView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
NavigationStackChecker(isInside: $isInsideNavigationStack)
|
||||||
|
)
|
||||||
|
|
||||||
|
if isInsideNavigationStack {
|
||||||
|
content
|
||||||
|
} else {
|
||||||
|
NavigationStack {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
15
Workouts/Cepo/NavigationStackChecker.swift
Normal file
15
Workouts/Cepo/NavigationStackChecker.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NavigationStackChecker: UIViewControllerRepresentable {
|
||||||
|
@Binding var isInside: Bool
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIViewController {
|
||||||
|
let viewController = UIViewController()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isInside = viewController.navigationController != nil
|
||||||
|
}
|
||||||
|
return viewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||||
|
}
|
@ -16,7 +16,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView {
|
||||||
Text("Placeholder")
|
WorkoutsView()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Workout", systemImage: "figure.strengthtraining.traditional")
|
Label("Workout", systemImage: "figure.strengthtraining.traditional")
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class Exercise: ListableItem {
|
final class Exercise {
|
||||||
@Attribute(.unique) var name: String = ""
|
var name: String = ""
|
||||||
var setup: String = ""
|
var setup: String = ""
|
||||||
var descr: String = ""
|
var descr: String = ""
|
||||||
var sets: Int = 0
|
var sets: Int = 0
|
||||||
@ -31,3 +32,62 @@ final class Exercise: ListableItem {
|
|||||||
self.weight = weight
|
self.weight = weight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Exercise: EditableEntity {
|
||||||
|
static func createNew() -> Exercise {
|
||||||
|
return Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Setup")) {
|
||||||
|
TextEditor(text: $model.setup)
|
||||||
|
.frame(minHeight: 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Weight")) {
|
||||||
|
HStack {
|
||||||
|
Text("\(model.weight)")
|
||||||
|
.bold()
|
||||||
|
Text("lbs")
|
||||||
|
Spacer()
|
||||||
|
Stepper("", value: $model.weight, in: 0...1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class ExerciseType: ListableItem {
|
final class ExerciseType {
|
||||||
@Attribute(.unique) var name: String = ""
|
var name: String = ""
|
||||||
var descr: String = ""
|
var descr: String = ""
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
@ -14,3 +15,34 @@ final class ExerciseType: ListableItem {
|
|||||||
self.descr = descr
|
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,9 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class Muscle: ListableItem {
|
final class Muscle {
|
||||||
@Attribute(.unique) var name: String = ""
|
var name: String = ""
|
||||||
|
|
||||||
var descr: String = ""
|
var descr: String = ""
|
||||||
|
|
||||||
@ -13,9 +14,60 @@ final class Muscle: ListableItem {
|
|||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
var exercises: [Exercise]? = []
|
var exercises: [Exercise]? = []
|
||||||
|
|
||||||
init(name: String, descr: String, muscleGroup: MuscleGroup) {
|
init(name: String, descr: String, muscleGroup: MuscleGroup? = nil) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.descr = descr
|
self.descr = descr
|
||||||
self.muscleGroup = muscleGroup
|
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,9 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class MuscleGroup: ListableItem {
|
final class MuscleGroup {
|
||||||
@Attribute(.unique) var name: String = ""
|
var name: String = ""
|
||||||
var descr: String = ""
|
var descr: String = ""
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
@ -14,3 +15,35 @@ final class MuscleGroup: ListableItem {
|
|||||||
self.descr = descr
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class Split: ListableItem {
|
final class Split {
|
||||||
@Attribute(.unique) var name: String = ""
|
var name: String = ""
|
||||||
var intro: String = ""
|
var intro: String = ""
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
|
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
|
||||||
@ -17,3 +18,80 @@ final class Split: ListableItem {
|
|||||||
self.intro = intro
|
self.intro = intro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - EditableEntity Conformance
|
||||||
|
|
||||||
|
extension Split: EditableEntity {
|
||||||
|
var count: Int? {
|
||||||
|
return self.exercises?.count
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createNew() -> Split {
|
||||||
|
return Split(name: "", intro: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var navigationTitle: String {
|
||||||
|
return "Splits"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
static func formView(for model: Split) -> some View {
|
||||||
|
EntityAddEditView(model: model) { $model in
|
||||||
|
SplitFormView(model: $model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Form View
|
||||||
|
|
||||||
|
fileprivate struct SplitFormView: View {
|
||||||
|
@Binding var model: Split
|
||||||
|
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(header: Text("Name")) {
|
||||||
|
TextField("Name", text: $model.name)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Description")) {
|
||||||
|
TextEditor(text: $model.intro)
|
||||||
|
.frame(minHeight: 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Exercises")) {
|
||||||
|
if let assignments = model.exercises, !assignments.isEmpty {
|
||||||
|
ForEach(assignments) { item in
|
||||||
|
ListItem(
|
||||||
|
title: item.exercise?.name ?? "Unnamed",
|
||||||
|
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
|
||||||
|
)
|
||||||
|
.swipeActions {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
itemToDelete = item
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No exercises added yet.")
|
||||||
|
}
|
||||||
|
Button(action: {
|
||||||
|
// TODO: Implement add exercise functionality
|
||||||
|
}) {
|
||||||
|
Label("Add Exercise", systemImage: "plus.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,4 +17,8 @@ final class Workout {
|
|||||||
self.end = end
|
self.end = end
|
||||||
self.split = split
|
self.split = split
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
start.formattedDate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ final class WorkoutLog {
|
|||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
var exercise: Exercise?
|
var exercise: Exercise?
|
||||||
|
|
||||||
init(date: Date, sets: Int, reps: Int, weight: Int, completed: Bool, workout: Workout, exercise: Exercise) {
|
init(workout: Workout, exercise: Exercise, date: Date, sets: Int, reps: Int, weight: Int, completed: Bool) {
|
||||||
self.date = date
|
self.date = date
|
||||||
self.sets = sets
|
self.sets = sets
|
||||||
self.reps = reps
|
self.reps = reps
|
||||||
|
@ -2,7 +2,10 @@ import Foundation
|
|||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
struct InitialData {
|
struct InitialData {
|
||||||
static let logger = AppLogger(subsystem: "Workouts", category: "InitialData")
|
static let logger = AppLogger(
|
||||||
|
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
|
||||||
|
category: "InitialData"
|
||||||
|
)
|
||||||
|
|
||||||
// Data structures for JSON decoding
|
// Data structures for JSON decoding
|
||||||
private struct ExerciseTypeData: Codable {
|
private struct ExerciseTypeData: Codable {
|
||||||
|
@ -6,10 +6,11 @@ final class WorkoutsContainer {
|
|||||||
|
|
||||||
static func create(shouldCreateDefaults: inout Bool) -> ModelContainer {
|
static func create(shouldCreateDefaults: inout Bool) -> ModelContainer {
|
||||||
let schema = Schema(versionedSchema: SchemaV1.self)
|
let schema = Schema(versionedSchema: SchemaV1.self)
|
||||||
let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self)
|
let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
|
||||||
|
let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self, configurations: [configuration])
|
||||||
|
|
||||||
let context = ModelContext(container)
|
let context = ModelContext(container)
|
||||||
let descriptor = FetchDescriptor<Workout>()
|
let descriptor = FetchDescriptor<Exercise>()
|
||||||
let results = try! context.fetch(descriptor)
|
let results = try! context.fetch(descriptor)
|
||||||
|
|
||||||
if results.isEmpty {
|
if results.isEmpty {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct AppLogger {
|
struct AppLogger {
|
||||||
private let logger: Logger
|
private let logger: Logger
|
||||||
@ -34,4 +35,24 @@ struct AppLogger {
|
|||||||
func error(_ message: String) {
|
func error(_ message: String) {
|
||||||
logger.error("\(formattedMessage(message))")
|
logger.error("\(formattedMessage(message))")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func vdebug(_ message: String) -> any View {
|
||||||
|
logger.debug("\(formattedMessage(message))")
|
||||||
|
return EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func vinfo(_ message: String) -> any View {
|
||||||
|
logger.info("\(formattedMessage(message))")
|
||||||
|
return EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func vwarning(_ message: String) -> any View {
|
||||||
|
logger.warning("\(formattedMessage(message))")
|
||||||
|
return EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func verror(_ message: String) -> any View {
|
||||||
|
logger.error("\(formattedMessage(message))")
|
||||||
|
return EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,21 +19,22 @@ struct ListItem: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack (alignment: .leading) {
|
VStack (alignment: .leading) {
|
||||||
Text("\(title)")
|
Text("\(title)")
|
||||||
HStack {
|
.font(.headline)
|
||||||
if let subtitle = subtitle {
|
HStack (alignment: .bottom) {
|
||||||
Text("\(subtitle)")
|
|
||||||
.font(.footnote)
|
|
||||||
}
|
|
||||||
if let badges = badges {
|
if let badges = badges {
|
||||||
ForEach (badges, id: \.self) { badge in
|
ForEach (badges, id: \.self) { badge in
|
||||||
Text("\(badge.text)")
|
Text("\(badge.text)")
|
||||||
.bold()
|
.bold()
|
||||||
.padding([.leading,.trailing], 5)
|
.padding([.leading,.trailing], 5)
|
||||||
.cornerRadius(4)
|
|
||||||
.background(badge.color)
|
.background(badge.color)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let subtitle = subtitle {
|
||||||
|
Text("\(subtitle)")
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let count = count {
|
if let count = count {
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// ExerciseTypeAddEditView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 11:33 AM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ExerciseTypeAddEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Bindable var model: ExerciseType
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section (header: Text("Nname")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("Save") {
|
|
||||||
try? modelContext.save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
//
|
|
||||||
// ExerciseTypeListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 11:27 AM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ExerciseTypeListView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var items: [ExerciseType]
|
|
||||||
|
|
||||||
@State var itemToEdit: ExerciseType? = nil
|
|
||||||
@State var itemToDelete: ExerciseType? = nil
|
|
||||||
|
|
||||||
private func save () {
|
|
||||||
try? modelContext.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
List {
|
|
||||||
ForEach (items) { item in
|
|
||||||
ListItem(title: item.name)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button (role: .destructive) {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Exercise Types")
|
|
||||||
.sheet(item: $itemToEdit) {item in
|
|
||||||
ExerciseTypeAddEditView(model: item)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Delete?",
|
|
||||||
isPresented: Binding<Bool>(
|
|
||||||
get: { itemToDelete != nil },
|
|
||||||
set: { if !$0 { itemToDelete = nil } }
|
|
||||||
),
|
|
||||||
titleVisibility: .visible
|
|
||||||
) {
|
|
||||||
Button("Delete", role: .destructive) {
|
|
||||||
if let itemToDelete = itemToDelete {
|
|
||||||
modelContext.delete(itemToDelete)
|
|
||||||
try? modelContext.save()
|
|
||||||
self.itemToDelete = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ExerciseAddEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
|
|
||||||
|
|
||||||
@State var model: Exercise
|
|
||||||
|
|
||||||
init(model: Exercise? = nil) {
|
|
||||||
_model = State(initialValue: model ?? Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
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)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Section (header: Text("Setup")) {
|
|
||||||
TextEditor(text: $model.setup)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
// } footer: {
|
|
||||||
// Text("Describe concisely how equipment should be configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
Section (header: Text("Weight")) {
|
|
||||||
HStack {
|
|
||||||
Text("\(model.weight)")
|
|
||||||
.bold()
|
|
||||||
Text("lbs")
|
|
||||||
Spacer()
|
|
||||||
Stepper("", value: $model.weight, in: 0...1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Section(header: Text("Target Muscles")) {
|
|
||||||
// Button(action: {
|
|
||||||
// showingMuscleSelection = true
|
|
||||||
// }) {
|
|
||||||
// HStack {
|
|
||||||
// if selectedMuscles.isEmpty {
|
|
||||||
// Text("None selected")
|
|
||||||
// .foregroundColor(.secondary)
|
|
||||||
// } else {
|
|
||||||
// Text(selectedMuscles.map { $0.name }.joined(separator: ", "))
|
|
||||||
// .foregroundColor(.primary)
|
|
||||||
// .multilineTextAlignment(.leading)
|
|
||||||
// }
|
|
||||||
// Spacer()
|
|
||||||
// Image(systemName: "chevron.right")
|
|
||||||
// .foregroundColor(.secondary)
|
|
||||||
// .font(.caption)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("Save") {
|
|
||||||
try? modelContext.save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
//
|
|
||||||
// ExercisesListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 4:30 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ExercisesListView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var groups: [ExerciseType]
|
|
||||||
|
|
||||||
@State var showingAddSheet = false
|
|
||||||
@State var itemToEdit: Exercise? = nil
|
|
||||||
@State var itemToDelete: Exercise? = nil
|
|
||||||
|
|
||||||
private func save () {
|
|
||||||
try? modelContext.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
ForEach (groups) { group in
|
|
||||||
let items = group.exercises ?? []
|
|
||||||
let itemCount = items.count
|
|
||||||
if itemCount > 0 {
|
|
||||||
Section (header: Text("\(group.name) (\(itemCount))")) {
|
|
||||||
ForEach (items) { item in
|
|
||||||
ListItem(title: item.name)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button (role: .destructive) {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Exercises")
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button(action: {
|
|
||||||
showingAddSheet.toggle()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingAddSheet) {
|
|
||||||
ExerciseAddEditView()
|
|
||||||
}
|
|
||||||
.sheet(item: $itemToEdit) { item in
|
|
||||||
ExerciseAddEditView(model: item)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Delete?",
|
|
||||||
isPresented: Binding<Bool>(
|
|
||||||
get: { itemToDelete != nil },
|
|
||||||
set: { if !$0 { itemToDelete = nil } }
|
|
||||||
),
|
|
||||||
titleVisibility: .visible
|
|
||||||
) {
|
|
||||||
Button("Delete", role: .destructive) {
|
|
||||||
if let itemToDelete = itemToDelete {
|
|
||||||
modelContext.delete(itemToDelete)
|
|
||||||
try? modelContext.save()
|
|
||||||
self.itemToDelete = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
//
|
|
||||||
// MuscleGroupAddEditView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
struct MuscleGroupAddEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@State var model: MuscleGroup
|
|
||||||
|
|
||||||
init(model: MuscleGroup? = nil) {
|
|
||||||
_model = State(initialValue: model ?? MuscleGroup(name: "", descr: ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section (header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("Save") {
|
|
||||||
if model.modelContext == nil {
|
|
||||||
modelContext.insert(model)
|
|
||||||
}
|
|
||||||
try? modelContext.save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
//
|
|
||||||
// MuscleAddEditView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 11:55 AM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct MuscleAddEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
|
|
||||||
@Bindable var model: Muscle
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
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)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("Save") {
|
|
||||||
try? modelContext.save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
//
|
|
||||||
// MuscleGroupsListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct MusclesListView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var groups: [MuscleGroup]
|
|
||||||
|
|
||||||
@State var itemToEdit: Muscle? = nil
|
|
||||||
@State var itemToDelete: Muscle? = nil
|
|
||||||
|
|
||||||
private func save () {
|
|
||||||
try? modelContext.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
ForEach (groups) { group in
|
|
||||||
Section (header: Text("\(group.name) (\(group.muscles?.count ?? 0))")) {
|
|
||||||
let items = group.muscles ?? []
|
|
||||||
ForEach (items) { item in
|
|
||||||
ListItem(title: item.name)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button (role: .destructive) {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Muscles")
|
|
||||||
.sheet(item: $itemToEdit) { item in
|
|
||||||
MuscleAddEditView(model: item)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Delete?",
|
|
||||||
isPresented: Binding<Bool>(
|
|
||||||
get: { itemToDelete != nil },
|
|
||||||
set: { if !$0 { itemToDelete = nil } }
|
|
||||||
),
|
|
||||||
titleVisibility: .visible
|
|
||||||
) {
|
|
||||||
Button("Delete", role: .destructive) {
|
|
||||||
if let itemToDelete = itemToDelete {
|
|
||||||
modelContext.delete(itemToDelete)
|
|
||||||
try? modelContext.save()
|
|
||||||
self.itemToDelete = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -75,3 +75,33 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ExercisesListView: View {
|
||||||
|
var body: some View {
|
||||||
|
EntityListView<Exercise>(sort: [SortDescriptor(\Exercise.name)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExerciseTypeListView: View {
|
||||||
|
var body: some View {
|
||||||
|
EntityListView<ExerciseType>(sort: [SortDescriptor(\ExerciseType.name)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MuscleGroupsListView: View {
|
||||||
|
var body: some View {
|
||||||
|
EntityListView<MuscleGroup>(sort: [SortDescriptor(\MuscleGroup.name)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MusclesListView: View {
|
||||||
|
var body: some View {
|
||||||
|
EntityListView<Muscle>(sort: [SortDescriptor(\Muscle.name)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SplitsListView: View {
|
||||||
|
var body: some View {
|
||||||
|
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SplitAddEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Bindable var model: Split
|
|
||||||
|
|
||||||
@State var itemToEdit: SplitExerciseAssignment? = nil
|
|
||||||
@State var itemToDelete: SplitExerciseAssignment? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section (header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.intro)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Exercises")) {
|
|
||||||
let item = model
|
|
||||||
if let assignments = item.exercises, !assignments.isEmpty {
|
|
||||||
ForEach(assignments, id: \.id) { item in
|
|
||||||
List {
|
|
||||||
ListItem(
|
|
||||||
title: item.exercise?.name ?? "Unnamed",
|
|
||||||
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
|
|
||||||
)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
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")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
|
|
||||||
}) {
|
|
||||||
Label("Add Exercise", systemImage: "plus.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("Save") {
|
|
||||||
try? modelContext.save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
//
|
|
||||||
// SplitsListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/13/25 at 10:27 AM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct SplitsListView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\Split.name)]) var items: [Split]
|
|
||||||
|
|
||||||
@State var itemToEdit: Split? = nil
|
|
||||||
@State var itemToDelete: Split? = nil
|
|
||||||
|
|
||||||
private func save () {
|
|
||||||
try? modelContext.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
List {
|
|
||||||
ForEach (items) { item in
|
|
||||||
ListItem(
|
|
||||||
title: item.name,
|
|
||||||
count: item.exercises?.count
|
|
||||||
)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button (role: .destructive) {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Muscle Groups")
|
|
||||||
}
|
|
||||||
.sheet(item: $itemToEdit) {item in
|
|
||||||
SplitAddEditView(model: item)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Delete?",
|
|
||||||
isPresented: Binding<Bool>(
|
|
||||||
get: { itemToDelete != nil },
|
|
||||||
set: { if !$0 { itemToDelete = nil } }
|
|
||||||
),
|
|
||||||
titleVisibility: .visible
|
|
||||||
) {
|
|
||||||
Button("Delete", role: .destructive) {
|
|
||||||
if let itemToDelete = itemToDelete {
|
|
||||||
modelContext.delete(itemToDelete)
|
|
||||||
try? modelContext.save()
|
|
||||||
self.itemToDelete = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
49
Workouts/Views/Workouts/ExercisePickerView.swift
Normal file
49
Workouts/Views/Workouts/ExercisePickerView.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// SplitPickerView.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(\.modelContext) private var modelContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
||||||
|
|
||||||
|
var onExerciseSelected: (Exercise) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
VStack {
|
||||||
|
Form {
|
||||||
|
Section (header: Text("This Split")) {
|
||||||
|
List {
|
||||||
|
ForEach(exercises) { exercise in
|
||||||
|
Button(action: {
|
||||||
|
onExerciseSelected(exercise)
|
||||||
|
dismiss()
|
||||||
|
}) {
|
||||||
|
ListItem(title: exercise.name)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
Workouts/Views/Workouts/SplitPickerView.swift
Normal file
73
Workouts/Views/Workouts/SplitPickerView.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// SplitPickerView.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 SplitPickerView: View {
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
|
||||||
|
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
||||||
|
|
||||||
|
var onSplitSelected: (Split) -> Void
|
||||||
|
// var onExerciseSelected: (Exercise) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
VStack {
|
||||||
|
Form {
|
||||||
|
Section (header: Text("This Split")) {
|
||||||
|
List {
|
||||||
|
ForEach(splits) { split in
|
||||||
|
Button(action: {
|
||||||
|
onSplitSelected(split)
|
||||||
|
dismiss()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(split.name)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Text("\(split.exercises?.count ?? 0)")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section (header: Text("Additional Exercises")) {
|
||||||
|
// List {
|
||||||
|
// ForEach(exercises) { exercise in
|
||||||
|
// Button(action: {
|
||||||
|
// onExerciseSelected(exercise)
|
||||||
|
// dismiss()
|
||||||
|
// }) {
|
||||||
|
// Text(exercise.name)
|
||||||
|
// }
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// .buttonStyle(.plain)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
Workouts/Views/Workouts/WorkoutLogEditView.swift
Normal file
103
Workouts/Views/Workouts/WorkoutLogEditView.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// WorkoutAddEditView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/13/25 at 9:13 PM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct WorkoutLogEditView: View {
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State var workoutLog: WorkoutLog
|
||||||
|
@State private var showingSaveConfirmation = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section (header: Text("Exercise")) {
|
||||||
|
Text("\(workoutLog.exercise?.name ?? "Unnamed Exercise")")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Sets/Reps")) {
|
||||||
|
Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10)
|
||||||
|
Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Weight")) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Text("\(workoutLog.weight) lbs")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Stepper("±1", value: $workoutLog.weight, in: 0...1000)
|
||||||
|
Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5)
|
||||||
|
}
|
||||||
|
.frame(width: 130)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Save") {
|
||||||
|
showingSaveConfirmation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) {
|
||||||
|
Button("Save Workout Log Only") {
|
||||||
|
try? modelContext.save()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Save Workout Log and Update Split") {
|
||||||
|
// Save the workout log
|
||||||
|
try? modelContext.save()
|
||||||
|
|
||||||
|
// Update the split with this workout log's data
|
||||||
|
// Note: Implementation depends on how splits are updated in your app
|
||||||
|
updateSplit(from: workoutLog)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
// Do nothing, dialog will dismiss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSplit(from workoutLog: WorkoutLog) {
|
||||||
|
let split = workoutLog.workout?.split
|
||||||
|
|
||||||
|
// Find the matching exercise in split.exercises by name
|
||||||
|
// if let exercises = split?.exercises {
|
||||||
|
// for exerciseAssignment in exercises {
|
||||||
|
// if exerciseAssignment.exercise.name == workoutLog.exercise.name {
|
||||||
|
// // Update the sets, reps, and weight in the split exercise assignment
|
||||||
|
// exerciseAssignment.sets = workoutLog.sets
|
||||||
|
// exerciseAssignment.reps = workoutLog.reps
|
||||||
|
// exerciseAssignment.weight = workoutLog.weight
|
||||||
|
//
|
||||||
|
// // Save the changes to the split
|
||||||
|
// try? modelContext.save()
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
125
Workouts/Views/Workouts/WorkoutLogView.swift
Normal file
125
Workouts/Views/Workouts/WorkoutLogView.swift
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
//
|
||||||
|
// WorkoutLogView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/13/25 at 6:58 PM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WorkoutLogView: View {
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
|
@State var workout: Workout
|
||||||
|
|
||||||
|
@State private var showingAddSheet = false
|
||||||
|
@State private var itemToEdit: WorkoutLog? = nil
|
||||||
|
@State private var itemToDelete: WorkoutLog? = nil
|
||||||
|
|
||||||
|
var sortedWorkoutLogs: [WorkoutLog] {
|
||||||
|
if let logs = workout.logs {
|
||||||
|
logs.sorted(by: { $0.exercise!.name < $1.exercise!.name })
|
||||||
|
} else {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
List {
|
||||||
|
ForEach (sortedWorkoutLogs) { log in
|
||||||
|
let badges = log.completed ? [Badge(text: "Completed", color: .green)] : []
|
||||||
|
ListItem(
|
||||||
|
title: log.exercise?.name ?? "Untitled Exercise",
|
||||||
|
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs",
|
||||||
|
badges: badges
|
||||||
|
)
|
||||||
|
.swipeActions(edge: .leading, allowsFullSwipe: false) {
|
||||||
|
if (log.completed) {
|
||||||
|
Button {
|
||||||
|
log.completed = false
|
||||||
|
try? modelContext.save()
|
||||||
|
} label: {
|
||||||
|
Label("Complete", systemImage: "circle.fill")
|
||||||
|
}
|
||||||
|
.tint(.green)
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
log.completed = true
|
||||||
|
try? modelContext.save()
|
||||||
|
} label: {
|
||||||
|
Label("Reset", systemImage: "checkmark.circle.fill")
|
||||||
|
}
|
||||||
|
.tint(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
itemToDelete = log
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
itemToEdit = log
|
||||||
|
} label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
.tint(.indigo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Workout")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: { showingAddSheet.toggle() }) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingAddSheet) {
|
||||||
|
ExercisePickerView { exercise in
|
||||||
|
let workoutLog = WorkoutLog(
|
||||||
|
workout: workout,
|
||||||
|
exercise: exercise,
|
||||||
|
date: Date(),
|
||||||
|
sets: exercise.sets,
|
||||||
|
reps: exercise.reps,
|
||||||
|
weight: exercise.weight,
|
||||||
|
completed: false
|
||||||
|
)
|
||||||
|
workout.logs?.append(workoutLog)
|
||||||
|
try? modelContext.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(item: $itemToEdit) { item in
|
||||||
|
WorkoutLogEditView(workoutLog: item)
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Delete?",
|
||||||
|
isPresented: Binding<Bool>(
|
||||||
|
get: { itemToDelete != nil },
|
||||||
|
set: { if !$0 { itemToDelete = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
if let item = itemToDelete {
|
||||||
|
withAnimation {
|
||||||
|
modelContext.delete(item)
|
||||||
|
try? modelContext.save()
|
||||||
|
itemToDelete = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
itemToDelete = nil
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
11
Workouts/Views/Workouts/WorkoutView.swift
Normal file
11
Workouts/Views/Workouts/WorkoutView.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WorkoutView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Workout View")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
WorkoutView()
|
||||||
|
}
|
150
Workouts/Views/Workouts/WorkoutsView.swift
Normal file
150
Workouts/Views/Workouts/WorkoutsView.swift
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// WorkoutsView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/13/25 at 6:52 PM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct WorkoutsView: View {
|
||||||
|
private let logger = AppLogger(
|
||||||
|
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
|
||||||
|
category: "WorkoutsView"
|
||||||
|
)
|
||||||
|
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
|
@Query(sort: [SortDescriptor(\Workout.start, order: .reverse)]) var workouts: [Workout]
|
||||||
|
|
||||||
|
@State private var showingSplitPicker = false
|
||||||
|
|
||||||
|
@State private var itemToDelete: Workout? = nil
|
||||||
|
@State private var itemToEdit: Workout? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
if workouts.isEmpty {
|
||||||
|
Text("No workouts yet")
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
ForEach (workouts) { workout in
|
||||||
|
NavigationLink(destination: WorkoutLogView(workout: workout)) {
|
||||||
|
ListItem(title: workout.label)
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
itemToDelete = workout
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
itemToEdit = workout
|
||||||
|
} label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
.tint(.indigo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Workouts")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button("Start Workout") {
|
||||||
|
showingSplitPicker = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .sheet(item: $itemToEdit) { item in
|
||||||
|
// T.formView(for: item)
|
||||||
|
// }
|
||||||
|
.confirmationDialog(
|
||||||
|
"Delete?",
|
||||||
|
isPresented: Binding<Bool>(
|
||||||
|
get: { itemToDelete != nil },
|
||||||
|
set: { if !$0 { itemToDelete = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
if let item = itemToDelete {
|
||||||
|
modelContext.delete(item)
|
||||||
|
try? modelContext.save()
|
||||||
|
itemToDelete = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
itemToDelete = nil
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Are you sure you want to delete this workout?")
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingSplitPicker) {
|
||||||
|
SplitPickerView { split in
|
||||||
|
let workout = Workout(start: Date(), split: split)
|
||||||
|
modelContext.insert(workout)
|
||||||
|
|
||||||
|
if let exercises = split.exercises {
|
||||||
|
for assignment in exercises {
|
||||||
|
if let exercise = assignment.exercise {
|
||||||
|
let workoutLog = WorkoutLog(
|
||||||
|
workout: workout,
|
||||||
|
exercise: exercise,
|
||||||
|
date: Date(),
|
||||||
|
sets: assignment.sets,
|
||||||
|
reps: assignment.reps,
|
||||||
|
weight: assignment.weight,
|
||||||
|
completed: false
|
||||||
|
)
|
||||||
|
modelContext.insert(workoutLog)
|
||||||
|
} else {
|
||||||
|
logger.debug("An exercise entity for a split is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try? modelContext.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
func formattedDate() -> String {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
let timeFormatter = DateFormatter()
|
||||||
|
timeFormatter.dateFormat = "h:mm a"
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
|
||||||
|
let date = self
|
||||||
|
|
||||||
|
if calendar.isDateInToday(date) {
|
||||||
|
return "Today @ \(timeFormatter.string(from: date))"
|
||||||
|
} else if calendar.isDateInYesterday(date) {
|
||||||
|
return "Yesterday @ \(timeFormatter.string(from: date))"
|
||||||
|
} else {
|
||||||
|
let dateComponents = calendar.dateComponents([.year], from: date)
|
||||||
|
let currentYearComponents = calendar.dateComponents([.year], from: now)
|
||||||
|
|
||||||
|
if dateComponents.year == currentYearComponents.year {
|
||||||
|
dateFormatter.dateFormat = "M/d"
|
||||||
|
} else {
|
||||||
|
dateFormatter.dateFormat = "M/d/yyyy"
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateString = dateFormatter.string(from: date)
|
||||||
|
let timeString = timeFormatter.string(from: date)
|
||||||
|
return "\(dateString) @ \(timeString)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -5,7 +5,9 @@
|
|||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<string>iCloud.com.dev.rzen.indie.Workouts</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.developer.icloud-services</key>
|
<key>com.apple.developer.icloud-services</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CloudKit</string>
|
<string>CloudKit</string>
|
||||||
|
Reference in New Issue
Block a user