wip
This commit is contained in:
@ -48,8 +48,11 @@ 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
|
||||
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Name")) {
|
||||
TextField("Name", text: $model.name)
|
||||
@ -62,37 +65,80 @@ fileprivate struct SplitFormView: View {
|
||||
}
|
||||
|
||||
Section(header: Text("Exercises")) {
|
||||
if let assignments = model.exercises, !assignments.isEmpty {
|
||||
ForEach(assignments) { item in
|
||||
ListItem(
|
||||
title: item.exercise?.name ?? Exercise.unnamed,
|
||||
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
|
||||
)
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
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,
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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
|
||||
.sheet (isPresented: $showingAddSheet) {
|
||||
ExercisePickerView { exercise in
|
||||
itemToEdit = SplitExerciseAssignment(
|
||||
order: 0,
|
||||
sets: exercise.sets,
|
||||
reps: exercise.reps,
|
||||
weight: exercise.weight,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} label: {
|
||||
ListItem(
|
||||
text: "Exercises",
|
||||
count: model.exercises?.count ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,18 +10,32 @@ struct CloudKitSyncObserver: ViewModifier {
|
||||
content
|
||||
.id(refreshID) // Force view refresh when this changes
|
||||
.onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in
|
||||
// When we receive a notification that CloudKit data changed:
|
||||
// 1. Create a new UUID to force view refresh
|
||||
refreshID = UUID()
|
||||
|
||||
// 2. Optionally, you can also manually refresh the model context
|
||||
// This is sometimes needed for complex relationships
|
||||
Task { @MainActor in
|
||||
try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||
// Add other model types as needed
|
||||
// do {
|
||||
// for entity in modelContext.container.schema.entities {
|
||||
// fetchAll<entity.Type>(of: entity.Type, from: modelContext)
|
||||
// }
|
||||
// } catch {
|
||||
// print("ERROR: failed to fetch data on CloudKit change")
|
||||
// }
|
||||
//
|
||||
// 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?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAll<T: PersistentModel>(of type: T.Type,from modelContext: ModelContext) async throws -> [T]? {
|
||||
try modelContext.fetch(FetchDescriptor<T>())
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to make it easier to use the modifier
|
||||
|
24
Workouts/Utils/BadgeView.swift
Normal file
24
Workouts/Utils/BadgeView.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// BadgeView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/14/25 at 2:20 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BadgeView: View {
|
||||
|
||||
var badge: Badge
|
||||
|
||||
var body: some View {
|
||||
Text("\(badge.text)")
|
||||
.bold()
|
||||
.padding([.leading,.trailing], 5)
|
||||
.background(badge.color)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
@ -10,7 +10,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ListItem: View {
|
||||
var title: String
|
||||
var title: String?
|
||||
var text: String?
|
||||
var subtitle: String?
|
||||
var count: Int?
|
||||
var badges: [Badge]? = []
|
||||
@ -18,17 +19,18 @@ struct ListItem: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack (alignment: .leading) {
|
||||
Text("\(title)")
|
||||
.font(.headline)
|
||||
if let title = title {
|
||||
Text("\(title)")
|
||||
.font(.headline)
|
||||
} else if let text = text {
|
||||
Text("\(text)")
|
||||
} else {
|
||||
Text("Untitled")
|
||||
}
|
||||
HStack (alignment: .bottom) {
|
||||
if let badges = badges {
|
||||
ForEach (badges, id: \.self) { badge in
|
||||
Text("\(badge.text)")
|
||||
.bold()
|
||||
.padding([.leading,.trailing], 5)
|
||||
.background(badge.color)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(4)
|
||||
BadgeView(badge: badge)
|
||||
}
|
||||
}
|
||||
if let subtitle = subtitle {
|
||||
@ -44,6 +46,7 @@ struct ListItem: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.frame(height: 40)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
//
|
||||
// SplitExerciseAssignment.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/15/25 at 7:12 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplitExerciseAssignmentAddEditView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var model: SplitExerciseAssignment
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Sets/Reps")) {
|
||||
Stepper("Sets: \(model.sets)", value: $model.sets, in: 1...10)
|
||||
Stepper("Reps: \(model.reps)", value: $model.reps, in: 1...50)
|
||||
}
|
||||
|
||||
// Weight section
|
||||
Section(header: Text("Weight")) {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
Text("\(model.weight) lbs")
|
||||
.font(.headline)
|
||||
}
|
||||
Spacer()
|
||||
VStack(alignment: .trailing) {
|
||||
Stepper("±1", value: $model.weight, in: 0...1000)
|
||||
Stepper("±5", value: $model.weight, in: 0...1000, step: 5)
|
||||
}
|
||||
.frame(width: 130)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,9 @@ struct ExercisePickerView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType]
|
||||
|
||||
// @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
||||
|
||||
var onExerciseSelected: (Exercise) -> Void
|
||||
|
||||
@ -22,18 +24,24 @@ struct ExercisePickerView: View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
Form {
|
||||
Section (header: Text("This Split")) {
|
||||
List {
|
||||
ForEach(exercises) { exercise in
|
||||
Button(action: {
|
||||
onExerciseSelected(exercise)
|
||||
dismiss()
|
||||
}) {
|
||||
ListItem(title: exercise.name)
|
||||
ForEach (exerciseTypes) { exerciseType in
|
||||
if let exercises = exerciseType.exercises, !exercises.isEmpty {
|
||||
let sortedExercises = exercises.sorted(by: { $0.name < $1.name })
|
||||
Section (header: Text("\(exerciseType.name)")) {
|
||||
List {
|
||||
ForEach(sortedExercises) { exercise in
|
||||
Button(action: {
|
||||
onExerciseSelected(exercise)
|
||||
dismiss()
|
||||
}) {
|
||||
ListItem(text: exercise.name)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ struct WorkoutEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var workout: Workout
|
||||
@State var endDateHidden: Bool = false
|
||||
@State var workoutEndDate: Date = Date()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Split")) {
|
||||
Text("\(workout.split?.name ?? Split.unnamed)")
|
||||
}
|
||||
// Section (header: Text("Split")) {
|
||||
// Text("\(workout.split?.name ?? Split.unnamed)")
|
||||
// }
|
||||
|
||||
Section (header: Text("Start/End")) {
|
||||
DatePicker("Started", selection: $workout.start)
|
||||
@ -31,38 +31,35 @@ struct WorkoutEditView: View {
|
||||
set: { newValue in
|
||||
withAnimation {
|
||||
if newValue {
|
||||
workout.end = Date()
|
||||
endDateHidden = false
|
||||
workoutEndDate = Date()
|
||||
workout.end = workoutEndDate
|
||||
} else {
|
||||
workout.end = nil
|
||||
endDateHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
if !endDateHidden {
|
||||
DatePicker("Ended", selection: $workout.start)
|
||||
if workout.end != nil {
|
||||
DatePicker("Ended", selection: $workoutEndDate)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
endDateHidden = workout.end == nil
|
||||
}
|
||||
|
||||
Section (header: Text("Workout Log")) {
|
||||
if let workoutLogs = workout.logs {
|
||||
List {
|
||||
ForEach (workoutLogs) { log in
|
||||
ListItem(
|
||||
title: log.exercise?.name ?? Exercise.unnamed,
|
||||
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("No workout logs yet")
|
||||
}
|
||||
}
|
||||
// Section (header: Text("Workout Log")) {
|
||||
// if let workoutLogs = workout.logs {
|
||||
// List {
|
||||
// ForEach (workoutLogs) { log in
|
||||
// ListItem(
|
||||
// title: log.exercise?.name ?? Exercise.unnamed,
|
||||
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// Text("No workout logs yet")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
|
@ -20,7 +20,9 @@ struct WorkoutLogView: View {
|
||||
|
||||
var sortedWorkoutLogs: [WorkoutLog] {
|
||||
if let logs = workout.logs {
|
||||
logs.sorted(by: { $0.exercise!.name < $1.exercise!.name })
|
||||
logs.sorted(by: {
|
||||
$0.completed == $1.completed ? $0.exercise!.name < $1.exercise!.name : !$0.completed
|
||||
})
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
@ -43,16 +45,20 @@ struct WorkoutLogView: View {
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: false) {
|
||||
if (log.completed) {
|
||||
Button {
|
||||
log.completed = false
|
||||
try? modelContext.save()
|
||||
withAnimation {
|
||||
log.completed = false
|
||||
try? modelContext.save()
|
||||
}
|
||||
} label: {
|
||||
Label("Complete", systemImage: "circle.fill")
|
||||
}
|
||||
.tint(.green)
|
||||
} else {
|
||||
Button {
|
||||
log.completed = true
|
||||
try? modelContext.save()
|
||||
withAnimation {
|
||||
log.completed = true
|
||||
try? modelContext.save()
|
||||
}
|
||||
} label: {
|
||||
Label("Reset", systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
@ -76,7 +82,7 @@ struct WorkoutLogView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed)")
|
||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showingAddSheet.toggle() }) {
|
||||
|
Reference in New Issue
Block a user