This commit is contained in:
2025-07-25 17:30:11 -04:00
parent 310c120ca3
commit 3fd6887ce7
55 changed files with 1062 additions and 649 deletions

View File

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "DumbBellIcon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@ -12,6 +13,7 @@
"value" : "dark"
}
],
"filename" : "DumbBellIcon 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@ -23,6 +25,7 @@
"value" : "tinted"
}
],
"filename" : "DumbBellIcon 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -5,8 +5,15 @@ import SwiftData
final class Workout {
var start: Date = Date()
var end: Date?
var status: WorkoutStatus? = WorkoutStatus.notStarted
var status: Int = 1
// var status: WorkoutStatus = WorkoutStatus.notStarted
//case notStarted = 1
//case inProgress = 2
//case completed = 3
//case skipped = 4
@Relationship(deleteRule: .nullify)
var split: Split?
@ -20,10 +27,25 @@ final class Workout {
}
var label: String {
if status == .completed, let endDate = end {
if status == 3, let endDate = end {
// if status == .completed, let endDate = end {
return "\(start.formattedDate())\(endDate.formattedDate())"
} else {
return start.formattedDate()
}
}
var statusName: String {
if status == 1 {
return "Not Started"
} else if status == 2 {
return "In Progress"
} else if status == 3 {
return "Completed"
} else if status == 4 {
return "Skipped"
} else {
return "In progress"
}
}
}

View File

@ -11,6 +11,9 @@ final class WorkoutLog {
var order: Int = 0
var exerciseName: String = ""
var currentStateIndex: Int? = nil
var elapsedSeconds: Int? = nil
var completed: Bool = false
@Relationship(deleteRule: .nullify)

View File

@ -10,21 +10,6 @@ struct WorkoutsMigrationPlan: SchemaMigrationPlan {
toVersion: SchemaV1.self,
willMigrate: { context in
print("migrating from v1 to v1")
let workouts = try? context.fetch(FetchDescriptor<Workout>())
workouts?.forEach { workout in
if let status = workout.status {
} else {
workout.status = .notStarted
}
// if let endDate = workout.end {
//
// } else {
// workout.end = Date()
// }
workout.end = Date()
}
},
didMigrate: { _ in
// No additional actions needed after migration

View File

@ -21,7 +21,7 @@ struct SplitAddEditView: View {
var body: some View {
NavigationStack {
Form {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()

View File

@ -0,0 +1,198 @@
//
// SplitDetailView.swift
// Workouts
//
// Created by rzen on 7/25/25 at 3:27PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitDetailView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State var split: Split
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: Exercise? = nil
@State private var createdWorkout: Workout? = nil
@State private var showingDeleteConfirmation: Bool = false
var body: some View {
NavigationStack {
Form {
Section (header: Text("What is a Split?")) {
Text("A “split” is simply how you divide (or “split up”) your weekly training across different days. Instead of working every muscle group every session, you assign certain muscle groups, movement patterns, or training emphases to specific days.")
.font(.caption)
}
Section (header: Text("Exercises")) {
List {
if let assignments = split.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.name < $1.name : $0.order < $1.order })
ForEach(sortedAssignments) { item in
ListItem(
title: item.name,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
)
.swipeActions {
Button {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
.onMove(perform: { indices, destination in
var exerciseArray = Array(sortedAssignments)
exerciseArray.move(fromOffsets: indices, toOffset: destination)
for (index, exercise) in exerciseArray.enumerated() {
exercise.order = index
}
if let modelContext = exerciseArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
Button {
showingAddSheet = true
} label: {
ListItem(title: "Add Exercise")
}
} else {
Text("No exercises added yet.")
Button(action: { showingAddSheet.toggle() }) {
ListItem(title: "Add Exercise")
}
}
}
}
Button ("Delete This Split", role: .destructive) {
showingDeleteConfirmation = true
}
.tint(.red)
}
.navigationTitle("\(split.name)")
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Start This Split") {
let workout = Workout(start: Date(), end: Date(), split: split)
modelContext.insert(workout)
if let exercises = split.exercises {
for assignment in exercises {
let workoutLog = WorkoutLog(
workout: workout,
exerciseName: assignment.name,
date: Date(),
order: assignment.order,
sets: assignment.sets,
reps: assignment.reps,
weight: assignment.weight
)
modelContext.insert(workoutLog)
}
}
try? modelContext.save()
// Set the created workout to trigger navigation
createdWorkout = workout
}
}
}
.navigationDestination(item: $createdWorkout, destination: { workout in
WorkoutLogListView(workout: workout)
})
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
let splitId = split.persistentModelID
print("exerciseNames: \(exerciseNames)")
if exerciseNames.count == 1 {
itemToEdit = Exercise(
split: split,
exerciseName: exerciseNames.first ?? "Exercise.unnamed",
order: 0,
sets: 3,
reps: 10,
weight: 40
)
} else {
for exerciseName in exerciseNames {
var duplicateExercise: [Exercise]? = nil
do {
duplicateExercise = try modelContext.fetch(FetchDescriptor<Exercise>(predicate: #Predicate{ exercise in
exerciseName == exercise.name && splitId == exercise.split?.persistentModelID
}))
} catch {
print("ERROR: failed to fetch \(exerciseName)")
}
if let dup = duplicateExercise, dup.count > 0 {
print("Skipping duplicate \(exerciseName) found \(dup.count) duplicate(s)")
} else {
print("Creating \(exerciseName) for \(split.name)")
modelContext.insert(Exercise(
split: split,
exerciseName: exerciseName,
order: 0,
sets: 3,
reps: 10,
weight: 40
))
}
}
}
try? modelContext.save()
}, allowMultiSelect: true)
}
.sheet(item: $itemToEdit) { item in
ExerciseAddEditView(model: item)
}
.confirmationDialog(
"Delete Exercise?",
isPresented: .constant(itemToDelete != nil),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
}
}
.confirmationDialog(
"Delete This Split?",
isPresented: $showingDeleteConfirmation,
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
modelContext.delete(split)
try? modelContext.save()
dismiss()
}
}
}
}

View File

@ -25,7 +25,7 @@ struct SplitsView: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink {
ExerciseListView(split: split)
SplitDetailView(split: split)
} label: {
SplitItem(
name: split.name,

View File

@ -40,7 +40,7 @@ struct WorkoutLogListView: View {
CheckboxListItem(
status: workoutLogStatus,
title: log.exerciseName,
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
let status = log.status ?? WorkoutStatus.notStarted
@ -89,6 +89,21 @@ struct WorkoutLogListView: View {
}
}
.onMove(perform: { indices, destination in
var workoutLogArray = Array(sortedWorkoutLogs)
workoutLogArray.move(fromOffsets: indices, toOffset: destination)
for (index, log) in workoutLogArray.enumerated() {
log.order = index
}
if let modelContext = workoutLogArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
}
}
}
@ -171,14 +186,14 @@ struct WorkoutLogListView: View {
if let _ = workout.logs?.first(where: { $0.status != .completed }) {
if let notStartedLogs = workout.logs?.filter({ $0.status == .notStarted }) {
if notStartedLogs.count == workout.logs?.count ?? 0 {
workout.status = .notStarted
workout.status = WorkoutStatus.notStarted.rawValue
}
}
if let _ = workout.logs?.first(where: { $0.status == .inProgress }) {
workout.status = .inProgress
workout.status = WorkoutStatus.inProgress.rawValue
}
} else {
workout.status = .completed
workout.status = WorkoutStatus.completed.rawValue
workout.end = Date()
}
try? modelContext.save()

View File

@ -7,6 +7,8 @@
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
enum WorkoutStatus: Int, Codable {
case notStarted = 1
case inProgress = 2

View File

@ -25,12 +25,12 @@ struct WorkoutEditView: View {
}
Section (header: Text("Status")) {
Text("\(workout.status?.name ?? WorkoutStatus.unnamed)")
Text("\(workout.statusName)")
}
Section (header: Text("Start/End")) {
DatePicker("Started", selection: $workout.start)
if workout.status == .completed {
if workout.status == WorkoutStatus.completed.rawValue {
DatePicker("Ended", selection: $workoutEndDate)
}
}
@ -46,7 +46,7 @@ struct WorkoutEditView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
if workout.status == .completed {
if workout.status == WorkoutStatus.completed.rawValue {
workout.end = workoutEndDate
}
dismiss()

View File

@ -37,8 +37,8 @@ struct WorkoutListView: View {
CalendarListItem(
date: workout.start,
title: workout.split?.name ?? Split.unnamed,
subtitle: "\(workout.status == .completed ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.status?.name ?? WorkoutStatus.unnamed)" )",
subtitle2: "\(workout.status?.name ?? WorkoutStatus.unnamed)"
subtitle: "\(workout.status == WorkoutStatus.completed.rawValue ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.statusName)" )",
subtitle2: "\(workout.statusName)"
)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {