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

@ -0,0 +1,62 @@
//
// ActiveWorkoutListView.swift
// Workouts
//
// Created by rzen on 7/20/25 at 6:35 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct ActiveWorkoutListView: View {
@Environment(\.modelContext) private var modelContext
let workouts: [Workout]
var body: some View {
List {
ForEach(workouts) { workout in
NavigationLink {
WorkoutDetailView(workout: workout)
} label: {
WorkoutCardView(workout: workout)
}
.listRowBackground(
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.2))
.padding(
EdgeInsets(
top: 4,
leading: 8,
bottom: 4,
trailing: 8
)
)
)
// .swipeActions (edge: .trailing, allowsFullSwipe: false) {
// Button {
// //
// } label: {
// Label("Delete", systemImage: "trash")
// .frame(height: 40)
// }
// .tint(.red)
// }
}
}
.listStyle(.carousel)
}
}
#Preview {
let container = AppContainer.preview
let split = Split(name: "Upper Body", color: "blue", systemImage: "figure.strengthtraining.traditional")
let workout1 = Workout(start: Date(), end: Date(), split: split)
let split2 = Split(name: "Lower Body", color: "red", systemImage: "figure.run")
let workout2 = Workout(start: Date().addingTimeInterval(-3600), end: Date(), split: split2)
ActiveWorkoutListView(workouts: [workout1, workout2])
.modelContainer(container)
}

View File

@ -0,0 +1,36 @@
//
// WorkoutCardView.swift
// Workouts
//
// Created by rzen on 7/22/25 at 9:54PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct WorkoutCardView: View {
let workout: Workout
var body: some View {
VStack(alignment: .leading) {
if let split = workout.split {
Image(systemName: split.systemImage)
.font(.system(size: 48))
.foregroundStyle(split.getColor())
} else {
Image(systemName: "dumbbell.fill")
.font(.system(size: 24))
.foregroundStyle(.gray)
}
Text(workout.split?.name ?? "Workout")
.font(.headline)
.foregroundStyle(.white)
Text(workout.statusName)
.font(.caption)
.foregroundStyle(Color.accentColor)
}
}
}

View File

@ -0,0 +1,50 @@
//
// WorkoutDetailView.swift
// Workouts
//
// Created by rzen on 7/22/25 at 9:54PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct WorkoutDetailView: View {
let workout: Workout
var body: some View {
VStack(alignment: .center, spacing: 8) {
if let logs = workout.logs?.sorted(by: { $0.order < $1.order }), !logs.isEmpty {
List {
ForEach(logs) { log in
NavigationLink {
ExerciseProgressControlView(log: log)
} label: {
WorkoutLogCardView(log: log)
}
.listRowBackground(
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.2))
.padding(
EdgeInsets(
top: 4,
leading: 8,
bottom: 4,
trailing: 8
)
)
)
}
}
.listStyle(.carousel)
} else {
Text("No exercises in this workout")
.font(.body)
.foregroundStyle(.secondary)
.padding()
Spacer()
}
}
}
}

View File

@ -0,0 +1,34 @@
//
// WorkoutLogCardView.swift
// Workouts
//
// Created by rzen on 7/22/25 at 9:56PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct WorkoutLogCardView: View {
let log: WorkoutLog
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(log.exerciseName)
.font(.headline)
.lineLimit(1)
Text(log.status?.name ?? "Not Started")
.font(.caption)
.foregroundStyle(Color.accentColor)
HStack(spacing: 12) {
Text("\(log.weight) lbs")
Spacer()
Text("\(log.sets) × \(log.reps)")
}
}
}
}

View File

@ -0,0 +1,69 @@
//
// WorkoutLogDetailView.swift
// Workouts
//
// Created by rzen on 7/22/25 at 9:57PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct WorkoutLogDetailView: View {
let log: WorkoutLog
var body: some View {
NavigationLink {
ExerciseProgressControlView(log: log)
} label: {
VStack(alignment: .center) {
Text(log.exerciseName)
.font(.title)
.lineLimit(1)
.minimumScaleFactor(0.5)
.layoutPriority(1)
HStack (alignment: .bottom) {
Text("\(log.weight)")
Text( "lbs")
.fontWeight(.light)
.padding([.trailing], 10)
Text("\(log.sets)")
Text("×")
.fontWeight(.light)
Text("\(log.reps)")
}
.font(.title3)
.lineLimit(1)
.minimumScaleFactor(0.5)
.layoutPriority(1)
Text(log.status?.name ?? "Not Started")
.foregroundStyle(Color.accentColor)
Text("Tap to start")
.foregroundStyle(Color.accentColor)
}
.padding()
}
.buttonStyle(.plain)
}
private func statusColor(for status: WorkoutStatus?) -> Color {
guard let status = status else { return .secondary }
switch status {
case .notStarted:
return .secondary
case .inProgress:
return .blue
case .completed:
return .green
case .skipped:
return .red
}
}
}

View File

@ -0,0 +1,117 @@
import SwiftUI
import SwiftData
struct WorkoutLogListView: View {
@Environment(\.modelContext) private var modelContext
let workout: Workout
@State private var selectedLogIndex: Int = 0
var body: some View {
VStack {
if let logs = workout.logs?.sorted(by: { $0.order < $1.order }), !logs.isEmpty {
TabView(selection: $selectedLogIndex) {
ForEach(Array(logs.enumerated()), id: \.element.id) { index, log in
WorkoutLogCard(log: log, index: index)
.tag(index)
}
}
.tabViewStyle(.page)
// .indexViewStyle(.page(backgroundDisplayMode: .always))
} else {
Text("No exercises in this workout")
.foregroundStyle(.secondary)
}
}
.navigationTitle(workout.split?.name ?? "Workout")
.navigationBarTitleDisplayMode(.inline)
}
}
struct WorkoutLogCard: View {
let log: WorkoutLog
let index: Int
var body: some View {
VStack(spacing: 12) {
Text(log.exerciseName)
.font(.headline)
.multilineTextAlignment(.center)
HStack {
VStack {
Text("\(log.sets)")
.font(.title2)
Text("Sets")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
VStack {
Text("\(log.reps)")
.font(.title2)
Text("Reps")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
VStack {
Text("\(log.weight)")
.font(.title2)
Text("Weight")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
}
NavigationLink {
ExerciseProgressView(log: log)
} label: {
Text("Start")
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.blue)
.cornerRadius(8)
}
.buttonStyle(PlainButtonStyle())
Text(log.status?.name ?? "Not Started")
.font(.caption)
.foregroundStyle(statusColor(for: log.status))
}
.padding()
.background(Color.secondary.opacity(0.2))
.cornerRadius(12)
.padding(.horizontal)
}
private func statusColor(for status: WorkoutStatus?) -> Color {
guard let status = status else { return .secondary }
switch status {
case .notStarted:
return .secondary
case .inProgress:
return .blue
case .completed:
return .green
case .skipped:
return .red
}
}
}
#Preview {
let container = AppContainer.preview
let workout = Workout(start: Date(), end: Date(), split: nil)
let log1 = WorkoutLog(workout: workout, exerciseName: "Bench Press", date: Date(), sets: 3, reps: 10, weight: 135)
let log2 = WorkoutLog(workout: workout, exerciseName: "Squats", date: Date(), order: 1, sets: 3, reps: 8, weight: 225)
return WorkoutLogListView(workout: workout)
.modelContainer(container)
}