Fix 28 issues from deep audit and UI audit + redesign changes
Some checks failed
Apple Platform CI / smoke-and-tests (push) Has been cancelled

Deep audit (issues 2-14):
- Add missing WCSession handlers for applicationContext and userInfo
- Fix BoundedFIFOQueue race condition with serial dispatch queue
- Fix timer race condition with main thread guarantee
- Fix watch pause state divergence — phone is now source of truth
- Fix wrong notification posted on logout (createdNewWorkout → userLoggedOut)
- Fix POST status check to accept any 2xx (was exact match)
- Fix @StateObject → @ObservedObject for injected viewModel
- Add pull-to-refresh to CompletedWorkoutsView
- Fix typos: RefreshUserInfoFetcable, defualtPackageModle
- Replace string concatenation with interpolation
- Replace 6 @StateObject with @ObservedObject for BridgeModule.shared
- Replace 7 hardcoded AVPlayer URLs with BaseURLs.currentBaseURL

UI audit (issues 1-15):
- Fix GeometryReader eating VStack space — replaced with .overlay
- Fix refreshable continuation resuming before fetch completes
- Remove duplicate @State workouts — derive from DataStore
- Decouple leaf views from BridgeModule (pass discrete values)
- Convert selectedIds from Array to Set for O(1) lookups
- Extract .sorted() from var body into computed properties
- Move search filter out of ForEach render loop
- Replace import SwiftUI with import Combine in non-UI classes
- Mark all @State properties private
- Extract L/R exercise auto-add logic to WorkoutViewModel
- Use enumerated() instead of .indices in ForEach
- Make AddSupersetView frame flexible instead of fixed 300pt
- Hoist Set construction out of per-exercise filter loop
- Move ViewModel network fetch from init to load()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-23 10:24:06 -06:00
parent 921829f2c3
commit 5d39dcb66f
60 changed files with 1783 additions and 1346 deletions

View File

@@ -11,98 +11,90 @@ struct ActionsView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var completedWorkout: (() -> Void)?
var planWorkout: ((Workout) -> Void)?
var workout: Workout
@Environment(\.dismiss) var dismiss
var showAddToCalendar: Bool
var startWorkoutAction: (() -> Void)
@State var showCompleteSheet: Bool = false
var body: some View {
HStack {
if bridgeModule.isInWorkout == false {
Button(action: {
bridgeModule.resetCurrentWorkout()
dismiss()
}, label: {
Image(systemName: "xmark.octagon.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
.foregroundColor(.white)
.accessibilityLabel("Close workout")
if showAddToCalendar {
GlassEffectContainer {
HStack(spacing: WerkoutTheme.sm) {
if bridgeModule.isInWorkout == false {
Button(action: {
planWorkout?(workout)
bridgeModule.resetCurrentWorkout()
dismiss()
}, label: {
Image(systemName: "calendar.badge.plus")
Image(systemName: "xmark.octagon.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
.foregroundColor(.white)
.accessibilityLabel("Plan workout")
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.danger)
.accessibilityLabel("Close workout")
if showAddToCalendar {
Button(action: {
planWorkout?(workout)
}, label: {
Image(systemName: "calendar.badge.plus")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.accent)
.accessibilityLabel("Plan workout")
}
Button(action: {
startWorkoutAction()
}, label: {
Image(systemName: "arrowtriangle.forward.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.success)
.accessibilityLabel("Start workout")
} else {
Button(action: {
showCompleteSheet.toggle()
}, label: {
Image(systemName: "checkmark")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.accent)
.accessibilityLabel("Complete workout")
Button(action: {
AudioEngine.shared.playFinished()
bridgeModule.pauseWorkout()
}, label: {
Image(systemName: bridgeModule.isPaused ? "play.circle.fill" : "pause.circle.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.glassEffect(.regular.interactive())
.tint(bridgeModule.isPaused ? WerkoutTheme.success : WerkoutTheme.warning)
.accessibilityLabel(bridgeModule.isPaused ? "Resume workout" : "Pause workout")
Button(action: {
AudioEngine.shared.playFinished()
nextExercise()
}, label: {
Image(systemName: "arrow.forward")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.success)
.accessibilityLabel("Next exercise")
}
Button(action: {
startWorkoutAction()
}, label: {
Image(systemName: "arrowtriangle.forward.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
.foregroundColor(.white)
.accessibilityLabel("Start workout")
} else {
Button(action: {
showCompleteSheet.toggle()
}, label: {
Image(systemName: "checkmark")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
.foregroundColor(.white)
.accessibilityLabel("Complete workout")
Button(action: {
AudioEngine.shared.playFinished()
bridgeModule.pauseWorkout()
}, label: {
bridgeModule.isPaused ?
Image(systemName: "play.circle.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
:
Image(systemName: "pause.circle.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(bridgeModule.isPaused ? .mint : .yellow)
.foregroundColor(.white)
.accessibilityLabel(bridgeModule.isPaused ? "Resume workout" : "Pause workout")
Button(action: {
AudioEngine.shared.playFinished()
nextExercise()
}, label: {
Image(systemName: "arrow.forward")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
.foregroundColor(.white)
.accessibilityLabel("Next exercise")
}
.padding(.horizontal, WerkoutTheme.sm)
}
.alert("Complete Workout", isPresented: $showCompleteSheet) {
Button("Complete Workout", role: .destructive) {
@@ -112,13 +104,12 @@ struct ActionsView: View {
Button("Back to workout", role: .cancel) { }
}
}
func nextExercise() {
bridgeModule.nextExercise()
}
}
struct ActionsView_Previews: PreviewProvider {
static var previews: some View {
ActionsView(workout: PreviewData.workout(), showAddToCalendar: true, startWorkoutAction: {})

View File

@@ -10,37 +10,41 @@ struct AddSupersetView: View {
@ObservedObject var createWorkoutSuperSet: CreateWorkoutSuperSet
var viewModel: WorkoutViewModel
@Binding var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
var body: some View {
TabView {
ForEach(createWorkoutSuperSet.exercises, id: \.id) { createWorkoutExercise in
VStack {
VStack(spacing: WerkoutTheme.sm) {
Text(createWorkoutExercise.exercise.name)
.font(.title2)
.font(WerkoutTheme.cardTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
.frame(maxWidth: .infinity)
if let side = createWorkoutExercise.exercise.side,
side.isEmpty == false {
Text(side)
.font(.title3)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
.frame(maxWidth: .infinity, alignment: .center)
}
CreateExerciseActionsView(workoutExercise: createWorkoutExercise,
superset: createWorkoutSuperSet,
viewModel: viewModel)
}
.frame(width: 300.0)
.frame(maxWidth: .infinity)
.padding(.horizontal, WerkoutTheme.md)
.padding(.bottom, 40)
}
}
.frame(height: 300)
.frame(minHeight: 220, maxHeight: 320)
.tabViewStyle(.page)
.background(WerkoutTheme.background)
.onAppear {
setupAppearance()
}
}
func setupAppearance() {
UIPageControl.appearance().currentPageIndicatorTintColor = .black
UIPageControl.appearance().pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.2)
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(WerkoutTheme.accent)
UIPageControl.appearance().pageIndicatorTintColor = UIColor(WerkoutTheme.accent).withAlphaComponent(0.25)
}
}

View File

@@ -12,11 +12,14 @@ struct AllEquipmentView: View {
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
var body: some View {
VStack {
VStack(spacing: WerkoutTheme.xs) {
if let _ = DataStore.shared.allEquipment {
Text("Select Equipment")
.foregroundColor(.cyan)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.accent)
Text("\(selectedEquipment.count) Selected")
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
}
}
.onTapGesture {

View File

@@ -12,9 +12,10 @@ struct AllExerciseView: View {
@Environment(\.dismiss) var dismiss
@State var searchString: String = ""
@Binding var filteredExercises: [Exercise]
@State private var displayedExercises: [Exercise] = []
var selectedExercise: ((Exercise) -> Void)
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4") ?? URL(fileURLWithPath: "/dev/null"))
@State var avPlayer = AVPlayer(url: URL(string: BaseURLs.currentBaseURL + "/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4") ?? URL(fileURLWithPath: "/dev/null"))
@State private var currentVideoURL: URL?
@State var videoExercise: Exercise? {
didSet {
@@ -24,62 +25,68 @@ struct AllExerciseView: View {
}
}
}
var body: some View {
VStack {
VStack(spacing: WerkoutTheme.sm) {
TextField("Filter", text: $searchString)
.padding(.horizontal)
.textFieldStyle(.roundedBorder)
.werkoutTextField()
.padding(.horizontal, WerkoutTheme.md)
List() {
ForEach(filteredExercises, id: \.self) { exercise in
if searchString.isEmpty || exercise.name.lowercased().contains(searchString.lowercased()) {
HStack {
VStack {
Text(exercise.name)
ForEach(displayedExercises, id: \.self) { exercise in
HStack {
VStack(spacing: WerkoutTheme.xs) {
Text(exercise.name)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
if let side = exercise.side,
side.isEmpty == false {
Text(side)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
if let side = exercise.side,
side.isEmpty == false {
Text(side)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if exercise.equipmentRequired?.isEmpty == false {
Text(exercise.spacedEquipmentRequired)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if exercise.muscleGroups?.isEmpty == false {
Text(exercise.spacedMuscleGroups)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedExercise(exercise)
dismiss()
if exercise.equipmentRequired?.isEmpty == false {
Text(exercise.spacedEquipmentRequired)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
Button(action: {
videoExercise = exercise
}) {
ZStack {
Circle()
.fill(.blue)
.frame(width: 33, height: 33)
Image(systemName: "video.fill")
.frame(width: 33, height: 33)
.foregroundColor(.white )
}
if exercise.muscleGroups?.isEmpty == false {
Text(exercise.spacedMuscleGroups)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(width: 33, height: 33)
}
.contentShape(Rectangle())
.onTapGesture {
selectedExercise(exercise)
dismiss()
}
Button(action: {
videoExercise = exercise
}) {
ZStack {
Circle()
.fill(WerkoutTheme.accent)
.frame(width: 33, height: 33)
Image(systemName: "video.fill")
.frame(width: 33, height: 33)
.foregroundStyle(WerkoutTheme.textPrimary)
}
}
.frame(width: 33, height: 33)
}
.listRowBackground(WerkoutTheme.surfaceCard)
}
}
.scrollContentBackground(.hidden)
.background(WerkoutTheme.background)
}
.sheet(item: $videoExercise) { exercise in
PlayerView(player: $avPlayer)
@@ -88,11 +95,23 @@ struct AllExerciseView: View {
avPlayer.play()
}
}
.onAppear { applySearch() }
.onChange(of: searchString) { _, _ in applySearch() }
.onChange(of: filteredExercises) { _, _ in applySearch() }
.onDisappear {
avPlayer.pause()
}
}
private func applySearch() {
if searchString.isEmpty {
displayedExercises = filteredExercises
} else {
let query = searchString.lowercased()
displayedExercises = filteredExercises.filter { $0.name.lowercased().contains(query) }
}
}
private func updatePlayer(for url: URL) {
if currentVideoURL == url {
avPlayer.seek(to: .zero)

View File

@@ -12,11 +12,14 @@ struct AllMusclesView: View {
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
var body: some View {
VStack {
VStack(spacing: WerkoutTheme.xs) {
if let _ = DataStore.shared.allMuscles {
Text("Select Muscles")
.foregroundColor(.cyan)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.accent)
Text("\(selectedMuscles.count) Selected")
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
}
}
.onTapGesture {

View File

@@ -1,37 +0,0 @@
//
// AllWorkoutPickerView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct AllWorkoutPickerView: View {
var mainViews: [MainViewTypes]
@Binding var selectedSegment: MainViewTypes
@StateObject var bridgeModule = BridgeModule.shared
var showCurrentWorkout: (() -> Void)
var body: some View {
HStack {
Picker("", selection: $selectedSegment) {
ForEach(mainViews, id: \.self) { viewType in
Text(viewType.title)
}
}
.pickerStyle(.segmented)
.padding([.top, .leading, .trailing])
if bridgeModule.isInWorkout {
Button(action: {
showCurrentWorkout()
}, label: {
Image(systemName: "figure.strengthtraining.traditional")
.padding(.trailing)
})
.tint(Color("appColor"))
}
}
}
}

View File

@@ -10,45 +10,47 @@ import SwiftUI
struct CaloriesBurnedView: View {
@Binding var healthKitWorkoutData: HealthKitWorkoutData?
let calsBurned: Double
var body: some View {
VStack {
VStack(spacing: WerkoutTheme.sm) {
HStack {
HStack {
HStack(spacing: WerkoutTheme.sm) {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
.foregroundStyle(.orange)
.font(.title)
VStack {
Text("\(calsBurned, specifier: "%.0f")")
}
Text("\(calsBurned, specifier: "%.0f")")
.font(.system(size: 28, weight: .black, design: .monospaced))
.foregroundStyle(WerkoutTheme.textPrimary)
}
.frame(maxWidth: .infinity)
}
if let minHeart = healthKitWorkoutData?.minHeartRate,
let maxHeart = healthKitWorkoutData?.maxHeartRate,
let avgHeart = healthKitWorkoutData?.avgHeartRate {
VStack {
HStack {
Image(systemName: "heart")
.foregroundColor(.red)
VStack(spacing: WerkoutTheme.xs) {
HStack(spacing: WerkoutTheme.sm) {
Image(systemName: "heart.fill")
.foregroundStyle(WerkoutTheme.danger)
.font(.title)
VStack {
HStack {
Text("\(minHeart, specifier: "%.0f")")
Text("-")
.foregroundStyle(WerkoutTheme.textMuted)
Text("\(maxHeart, specifier: "%.0f")")
}
Text("\(avgHeart, specifier: "%.0f")")
.font(.system(size: 20, weight: .bold, design: .monospaced))
.foregroundStyle(WerkoutTheme.textPrimary)
Text("avg \(avgHeart, specifier: "%.0f")")
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
}
}
}
.frame(maxWidth: .infinity)
}
}
.werkoutCard()
}
}
//#Preview {
// CaloriesBurnedView()
//}

View File

@@ -8,57 +8,95 @@
import SwiftUI
struct CompletedWorkoutsView: View {
@State var completedWorkouts: [CompletedWorkout]?
@State var showCompletedWorkouts: Bool = false
@State private var completedWorkouts: [CompletedWorkout]?
@State private var showCompletedWorkouts: Bool = false
@State private var loadError: String?
var body: some View {
VStack(alignment: .leading) {
if let completedWorkouts = completedWorkouts {
Divider()
.overlay(WerkoutTheme.divider)
Text("Workout History:")
.font(WerkoutTheme.sectionTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
HStack {
Text("Number of workouts:")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
Text("\(completedWorkouts.count)")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.accent)
}
if let lastWorkout = completedWorkouts.last {
HStack {
Text("Last workout:")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
Text(lastWorkout.workoutStartTime)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.accent)
}
Button("View All Workouts", action: {
showCompletedWorkouts = true
})
.font(.system(size: 16, weight: .bold))
.foregroundStyle(WerkoutTheme.textPrimary)
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(Constants.buttonRadius)
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.accent)
.clipShape(RoundedRectangle(cornerRadius: WerkoutTheme.buttonRadius, style: .continuous))
.padding()
.frame(maxWidth: .infinity)
}
} else {
if let loadError = loadError {
Text(loadError)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.danger)
} else {
Text("loading completed workouts")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textMuted)
}
}
}
.padding(WerkoutTheme.md)
.background(WerkoutTheme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: WerkoutTheme.cardRadius, style: .continuous))
.onAppear{
fetchCompletedWorkouts()
}
.refreshable {
await withCheckedContinuation { continuation in
CompletedWorkoutFetchable().fetch(completion: { result in
DispatchQueue.main.async {
switch result {
case .success(let model):
self.completedWorkouts = model
self.loadError = nil
case .failure(let failure):
self.loadError = "Unable to load workout history: \(failure.localizedDescription)"
}
continuation.resume()
}
})
}
}
.sheet(isPresented: $showCompletedWorkouts) {
if let completedWorkouts = completedWorkouts {
WorkoutHistoryView(completedWorkouts: completedWorkouts)
}
}
}
func fetchCompletedWorkouts() {
CompletedWorkoutFetchable().fetch(completion: { result in
switch result {

View File

@@ -8,15 +8,17 @@
import SwiftUI
struct CountdownView: View {
@StateObject var bridgeModule = BridgeModule.shared
let currentExerciseDuration: Int?
let currentExerciseTimeLeft: Int
var body: some View {
if let duration = bridgeModule.currentWorkoutInfo.currentExercise?.duration,
if let duration = currentExerciseDuration,
duration > 0 {
HStack {
if bridgeModule.currentExerciseTimeLeft >= 0 && duration > bridgeModule.currentExerciseTimeLeft {
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
.tint(Color("appColor"))
if currentExerciseTimeLeft >= 0 && duration > currentExerciseTimeLeft {
ProgressView(value: Float(currentExerciseTimeLeft), total: Float(duration))
.tint(currentExerciseTimeLeft < 5 ? WerkoutTheme.danger : WerkoutTheme.accent)
.animation(.easeInOut, value: currentExerciseTimeLeft < 5)
}
}
}
@@ -25,6 +27,6 @@ struct CountdownView: View {
struct CountdownView_Previews: PreviewProvider {
static var previews: some View {
CountdownView()
CountdownView(currentExerciseDuration: 30, currentExerciseTimeLeft: 15)
}
}

View File

@@ -11,7 +11,7 @@ struct CreateWorkoutSupersetView: View {
@Binding var showAddExercise: Bool
@ObservedObject var superset: CreateWorkoutSuperSet
@ObservedObject var viewModel: WorkoutViewModel
var body: some View {
Section(content: {
AddSupersetView(
@@ -19,50 +19,58 @@ struct CreateWorkoutSupersetView: View {
viewModel: viewModel,
selectedCreateWorkoutSuperSet: $selectedCreateWorkoutSuperSet)
}, header: {
VStack {
VStack(spacing: WerkoutTheme.sm) {
HStack {
TextField("Superset Title", text: $superset.title)
.font(.title2)
.font(WerkoutTheme.sectionTitle)
.foregroundStyle(WerkoutTheme.accent)
}
VStack {
VStack(spacing: WerkoutTheme.sm) {
HStack {
Text("Exercises: \(superset.exercises.count)")
.font(.subheadline)
.bold()
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
Spacer()
Button(action: {
selectedCreateWorkoutSuperSet = superset
showAddExercise = true
}, label: {
Image(systemName: "dumbbell.fill")
.font(.title2)
.foregroundStyle(WerkoutTheme.accent)
})
.accessibilityLabel("Add exercise")
.accessibilityHint("Adds an exercise to this superset")
Divider()
.overlay(WerkoutTheme.divider)
Button(action: {
viewModel.delete(superset: superset)
}, label: {
Image(systemName: "trash")
.font(.title2)
.foregroundStyle(WerkoutTheme.danger)
})
.accessibilityLabel("Delete superset")
.accessibilityHint("Removes this superset")
}
Divider()
.overlay(WerkoutTheme.divider)
Stepper(label: {
HStack {
Text("Rounds: ")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textPrimary)
Text("\(superset.numberOfRounds)")
.foregroundColor(superset.numberOfRounds > 0 ? Color(uiColor: .label) : .red)
.foregroundColor(superset.numberOfRounds > 0 ? WerkoutTheme.textPrimary : WerkoutTheme.danger)
.font(WerkoutTheme.bodyText)
.bold()
}
}, onIncrement: {
@@ -70,8 +78,10 @@ struct CreateWorkoutSupersetView: View {
}, onDecrement: {
superset.decreaseNumberOfRounds()
})
.tint(WerkoutTheme.accent)
}
}
.padding(.vertical, WerkoutTheme.xs)
})
}
}

View File

@@ -8,50 +8,55 @@
import SwiftUI
struct ExtCountdownView: View {
@StateObject var bridgeModule = BridgeModule.shared
@ObservedObject var bridgeModule = BridgeModule.shared
var body: some View {
GeometryReader { metrics in
VStack {
if let currenExercise = bridgeModule.currentWorkoutInfo.currentExercise {
HStack {
Text(currenExercise.exercise.extName)
.font(.system(size: 200))
.font(.system(size: 200, weight: .black, design: .rounded))
.foregroundColor(WerkoutTheme.textPrimary)
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(height: metrics.size.height * 0.5)
HStack {
if let duration = currenExercise.duration,
duration > 0 {
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
.scaleEffect(x: 1, y: 6, anchor: .center)
.tint(WerkoutTheme.accent)
Text("\(bridgeModule.currentExerciseTimeLeft)")
.font(Font.system(size: 100))
.font(WerkoutTheme.stat)
.foregroundColor(WerkoutTheme.accent)
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding(.leading)
.padding(.trailing, 100)
}
if let reps = currenExercise.reps,
reps > 0 {
Text(" X \(reps)")
.font(Font.system(size: 100))
.font(.system(size: 100, weight: .heavy, design: .monospaced))
.foregroundColor(WerkoutTheme.textPrimary)
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let weight = currenExercise.weight,
weight > 0 {
Text(" @ \(weight)")
.font(Font.system(size: 100))
.font(.system(size: 100, weight: .heavy, design: .monospaced))
.foregroundColor(WerkoutTheme.textSecondary)
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)

View File

@@ -10,7 +10,7 @@ import SwiftUI
struct ExtExerciseList: View {
var workout: Workout
var allSupersetExecerciseIndex: Int
var body: some View {
if let allSupersetExecercise = workout.allSupersetExecercise {
ZStack {
@@ -21,49 +21,41 @@ struct ExtExerciseList: View {
HStack {
if supersetExecerciseIdx == allSupersetExecerciseIndex {
Image(systemName: "figure.run")
.foregroundColor(Color("appColor"))
.foregroundColor(WerkoutTheme.accent)
.font(Font.system(size: 55))
.minimumScaleFactor(0.01)
.lineLimit(1)
}
Text(supersetExecercise.exercise.name)
.font(Font.system(size: 55))
.font(Font.system(size: 55, weight: supersetExecerciseIdx == allSupersetExecerciseIndex ? .bold : .medium))
.foregroundColor(supersetExecerciseIdx == allSupersetExecerciseIndex ? WerkoutTheme.textPrimary : WerkoutTheme.textSecondary)
.minimumScaleFactor(0.01)
.lineLimit(3)
.padding()
Spacer()
}
.id(supersetExecerciseIdx)
}
}
.onChange(of: allSupersetExecerciseIndex, perform: { newValue in
.onChange(of: allSupersetExecerciseIndex) {
withAnimation {
proxy.scrollTo(allSupersetExecerciseIndex, anchor: .top)
}
})
}
}
VStack {
Text("\(allSupersetExecerciseIndex+1)/\(workout.allSupersetExecercise?.count ?? 0)")
.font(Font.system(size: 55))
.font(Font.system(size: 55, weight: .bold, design: .monospaced))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.bold()
.foregroundColor(.white)
.background(
Capsule()
.strokeBorder(Color.black, lineWidth: 0.8)
.background(Color(uiColor: UIColor(red: 148/255,
green: 0,
blue: 211/255,
alpha: 0.5)))
.clipped()
)
.clipShape(Capsule())
.foregroundColor(WerkoutTheme.textPrimary)
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.accent)
Spacer()
}
}

View File

@@ -1,5 +1,5 @@
//
// FilterAllView.swift
// FilterChip.swift
// Werkout_ios
//
// Created by Trey Tartt on 11/25/24.
@@ -7,153 +7,26 @@
import SwiftUI
struct FilterAllView: View {
@Binding var selectedMuscles: Set<String>
@Binding var selectedEquipment: Set<String>
@Binding var searchNameString: String
@Binding var uniqueWorkoutUsers: [RegisteredUser]?
@Binding var filteredRegisterdUser: RegisteredUser?
@Binding var filteredWorkouts: [Workout]
@Binding var workouts: [Workout]
@Binding var currentSort: SortType?
var body: some View {
VStack {
TextField("Search by name" ,text: $searchNameString)
.padding(.horizontal)
.textFieldStyle(.roundedBorder)
struct FilterChip: View {
let label: String
let color: Color
let onRemove: () -> Void
HStack {
if let uniqueWorkoutUsers = uniqueWorkoutUsers {
Menu(content: {
ForEach(uniqueWorkoutUsers, id: \.self) { index in
Button(action: {
filteredRegisterdUser = index
filteredWorkouts = workouts.filterWorkouts(
nameSearchString: searchNameString,
musclesSearchString: selectedMuscles,
equipmentSearchString: selectedEquipment,
filteredRegisterdUser: filteredRegisterdUser
)
}, label: {
Text((index.firstName ?? "") + " -" + (index.lastName ?? ""))
})
}
Button(action: {
filteredRegisterdUser = nil
filteredWorkouts = workouts.filterWorkouts(
nameSearchString: searchNameString,
musclesSearchString: selectedMuscles,
equipmentSearchString: selectedEquipment,
filteredRegisterdUser: filteredRegisterdUser
)
}, label: {
Text("All")
})
}, label: {
Image(systemName: filteredRegisterdUser == nil ? "person.2" : "person.2.fill")
.padding(.trailing)
})
}
Menu(content: {
ForEach(SortType.allCases, id: \.self) { index in
Button(action: {
sortWorkouts(sortType: index)
}, label: {
Text(index.rawValue)
})
}
}, label: {
Image(systemName: "list.number")
.padding(.trailing)
})
if let equipments = DataStore.shared.allEquipment {
Menu(content: {
ForEach(equipments, id: \.id) { equipment in
Button(action: {
if selectedEquipment.contains(equipment.name) {
selectedEquipment.remove(equipment.name)
} else {
selectedEquipment.insert(equipment.name)
}
}, label: {
if selectedEquipment.contains(equipment.name) {
Image(systemName: "checkmark")
}
Text(equipment.name)
})
}
}, label: {
Image(systemName: "scalemass")
.padding(.trailing)
})
}
if let muscles = DataStore.shared.allMuscles {
Menu(content: {
ForEach(muscles, id: \.id) { muscle in
Button(action: {
if selectedMuscles.contains(muscle.name) {
selectedMuscles.remove(muscle.name)
} else {
selectedMuscles.insert(muscle.name)
}
}, label: {
if selectedMuscles.contains(muscle.name) {
Image(systemName: "checkmark")
}
Text(muscle.name)
})
}
}, label: {
Image(systemName: "brain.head.profile")
.padding(.trailing)
})
}
Button(action: {
selectedMuscles.removeAll()
selectedEquipment.removeAll()
searchNameString = ""
filteredRegisterdUser = nil
}, label: {
Image(systemName: "xmark.circle")
.padding(.trailing)
})
var body: some View {
HStack(spacing: WerkoutTheme.xs) {
Text(label)
.font(WerkoutTheme.caption)
.foregroundStyle(color)
Button(action: onRemove) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(color.opacity(0.7))
}
}
}
func sortWorkouts(sortType: SortType) {
if currentSort == sortType {
filteredWorkouts = filteredWorkouts.reversed()
return
}
switch sortType {
case .name:
filteredWorkouts = filteredWorkouts.sorted(by: {
$0.name < $1.name
})
case .createdDate:
filteredWorkouts = filteredWorkouts.sorted(by: {
$0.createdAt ?? Date() < $1.createdAt ?? Date()
})
}
currentSort = sortType
.padding(.horizontal, WerkoutTheme.sm + 2)
.padding(.vertical, WerkoutTheme.xs + 2)
.background(color.opacity(0.12))
.clipShape(Capsule())
}
}
#Preview {
FilterAllView(selectedMuscles: .constant([]),
selectedEquipment: .constant([]),
searchNameString: .constant(""),
uniqueWorkoutUsers: .constant(nil),
filteredRegisterdUser: .constant(nil),
filteredWorkouts: .constant(PreviewData.allWorkouts()),
workouts: .constant(PreviewData.allWorkouts()),
currentSort: .constant(nil))
}

View File

@@ -10,28 +10,27 @@ import SwiftUI
struct InfoView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var workout: Workout
var body: some View {
VStack {
VStack(alignment: .leading, spacing: WerkoutTheme.sm) {
Text(workout.name)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title3)
.padding()
.font(WerkoutTheme.sectionTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
if let desc = workout.description {
Text(desc)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding([.leading, .trailing])
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
}
if let estimatedTime = workout.estimatedTime {
Text(estimatedTime.asString(style: .abbreviated))
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding([.leading, .trailing])
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.accent)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}

View File

@@ -9,31 +9,37 @@ import SwiftUI
struct Logoutview: View {
@ObservedObject var userStore = UserStore.shared
var body: some View {
HStack {
Button("Logout", action: {
userStore.logout()
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.white)
.background(.red)
.cornerRadius(Constants.buttonRadius)
.padding()
.frame(maxWidth: .infinity)
Button(action: {
userStore.refreshUserData()
}, label: {
Image(systemName: "arrow.triangle.2.circlepath")
})
.frame(width: 44, height: 44)
.foregroundColor(.white)
.background(.green)
.cornerRadius(Constants.buttonRadius)
.padding()
.frame(maxWidth: .infinity)
GlassEffectContainer {
HStack {
Button("Logout", action: {
userStore.logout()
})
.font(.system(size: 16, weight: .bold))
.foregroundStyle(WerkoutTheme.textPrimary)
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.danger)
.clipShape(RoundedRectangle(cornerRadius: WerkoutTheme.buttonRadius, style: .continuous))
.padding()
.frame(maxWidth: .infinity)
Button(action: {
userStore.refreshUserData()
}, label: {
Image(systemName: "arrow.triangle.2.circlepath")
})
.font(.title)
.foregroundStyle(WerkoutTheme.textPrimary)
.frame(width: 44, height: 44)
.glassEffect(.regular.interactive())
.tint(WerkoutTheme.success)
.clipShape(RoundedRectangle(cornerRadius: WerkoutTheme.buttonRadius, style: .continuous))
.padding()
.frame(maxWidth: .infinity)
}
}
}
}

View File

@@ -9,21 +9,26 @@ import SwiftUI
struct NameView: View {
@ObservedObject var userStore = UserStore.shared
var body: some View {
if let registeredUser = userStore.registeredUser {
if let nickName = registeredUser.nickName {
Text(nickName)
.font(.title)
.font(WerkoutTheme.heroTitle)
.foregroundStyle(WerkoutTheme.accent)
}
HStack {
Text(registeredUser.firstName ?? "-")
Text(registeredUser.lastName ?? "-")
}
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
if let email = registeredUser.email {
Text(email)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
}
}
}

View File

@@ -10,9 +10,12 @@ import SwiftUI
struct OvalTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.background(LinearGradient(gradient: Gradient(colors: [Color(uiColor: .secondarySystemBackground), Color(uiColor: .secondarySystemBackground)]), startPoint: .topLeading, endPoint: .bottomTrailing))
.cornerRadius(20)
.shadow(color: Color(red: 120/255, green: 120/255, blue: 120/255, opacity: 1), radius: 5)
.padding(12)
.background(WerkoutTheme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: WerkoutTheme.buttonRadius, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: WerkoutTheme.buttonRadius, style: .continuous)
.strokeBorder(WerkoutTheme.accent.opacity(0.3), lineWidth: 1)
)
}
}

View File

@@ -10,46 +10,56 @@ import SwiftUI
struct PlannedWorkoutView: View {
let workouts: [PlannedWorkout]
@Binding var selectedPlannedWorkout: Workout?
private var sortedWorkouts: [PlannedWorkout] {
workouts.sorted(by: { $0.onDate < $1.onDate })
}
var body: some View {
List {
ForEach(workouts.sorted(by: { $0.onDate < $1.onDate }), id: \.id) { plannedWorkout in
Button(action: {
selectedPlannedWorkout = plannedWorkout.workout
}, label: {
HStack {
VStack(alignment: .leading) {
Text(plannedWorkout.onDate.plannedDate?.weekDay ?? "-")
.font(.title)
Text(plannedWorkout.onDate.plannedDate?.monthString ?? "-")
.font(.title)
Text(plannedWorkout.onDate.plannedDate?.dateString ?? "-")
.font(.title)
ScrollView {
LazyVStack(spacing: WerkoutTheme.sm) {
ForEach(sortedWorkouts, id: \.id) { plannedWorkout in
Button(action: {
selectedPlannedWorkout = plannedWorkout.workout
}, label: {
HStack(spacing: WerkoutTheme.md) {
VStack(spacing: WerkoutTheme.xs) {
Text(plannedWorkout.onDate.plannedDate?.weekDay ?? "-")
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.accent)
Text(plannedWorkout.onDate.plannedDate?.dateString ?? "-")
.font(.system(size: 28, weight: .black, design: .monospaced))
.foregroundStyle(WerkoutTheme.accent)
Text(plannedWorkout.onDate.plannedDate?.monthString ?? "-")
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textSecondary)
}
.frame(width: 60)
WerkoutTheme.divider.frame(width: 0.5)
VStack(alignment: .leading, spacing: WerkoutTheme.xs) {
Text(plannedWorkout.workout.name)
.font(WerkoutTheme.cardTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
Text(plannedWorkout.workout.description ?? "")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Divider()
VStack {
Text(plannedWorkout.workout.name)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
Text(plannedWorkout.workout.description ?? "")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
})
.buttonStyle(.plain)
.accessibilityLabel("Open planned workout \(plannedWorkout.workout.name)")
.accessibilityHint("Shows workout details")
.werkoutCard()
})
.buttonStyle(.plain)
.accessibilityLabel("Open planned workout \(plannedWorkout.workout.name)")
.accessibilityHint("Shows workout details")
}
}
.padding(.horizontal, WerkoutTheme.sm)
}
.scrollEdgeEffectStyle(.soft, for: .top)
}
}
//#Preview {
// PlannedWorkoutView()
//}

View File

@@ -9,23 +9,28 @@ import SwiftUI
struct RateWorkoutView: View {
@Binding var difficulty: Float
var body: some View {
VStack {
Divider()
.overlay(WerkoutTheme.divider)
HStack {
Text("No Rate")
.foregroundColor(.black)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textMuted)
Text("Easy")
.foregroundColor(.green)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.success)
Spacer()
Text("Death")
.foregroundColor(.red)
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.danger)
}
ZStack {
Slider(value: $difficulty, in: 0...5, step: 1)
.tint(WerkoutTheme.accent)
}
}
}

View File

@@ -13,7 +13,10 @@ struct ShowNextUpView: View {
var body: some View {
Toggle(isOn: $extShowNextVideo, label: {
Text("Show next up video")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textPrimary)
})
.tint(WerkoutTheme.accent)
}
}

View File

@@ -12,11 +12,13 @@ struct ThotPreferenceView: View {
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
var body: some View {
if userStore.registeredUser?.NSFWValue ?? false {
Group {
Text("Phone THOT Style:")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
Picker("Phone THOT Style:", selection: $phoneThotStyle) {
ForEach(ThotStyle.allCases, id: \.self) { style in
Text(style.stringValue())
@@ -24,10 +26,14 @@ struct ThotPreferenceView: View {
}
}
.pickerStyle(.segmented)
.tint(WerkoutTheme.accent)
Divider()
.overlay(WerkoutTheme.divider)
Text("External THOT Style:")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
Picker("External THOT Style:", selection: $extThotStyle) {
ForEach(ThotStyle.allCases, id: \.self) { style in
Text(style.stringValue())
@@ -35,10 +41,14 @@ struct ThotPreferenceView: View {
}
}
.pickerStyle(.segmented)
.tint(WerkoutTheme.accent)
if let genderOptions = DataStore.shared.nsfwGenderOptions {
Divider()
.overlay(WerkoutTheme.divider)
Text("Video Gender:")
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
Picker("Video Gender:", selection: $thotGenderOption) {
ForEach(genderOptions, id: \.self) { option in
Text(option.capitalized)
@@ -46,6 +56,7 @@ struct ThotPreferenceView: View {
}
}
.pickerStyle(.segmented)
.tint(WerkoutTheme.accent)
}
}
}

View File

@@ -9,18 +9,20 @@ import SwiftUI
struct TitleView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var body: some View {
HStack {
if let workout = bridgeModule.currentWorkoutInfo.workout {
Text(workout.name)
.font(Font.system(size: 100))
.font(.system(size: 100, weight: .black, design: .rounded))
.foregroundColor(WerkoutTheme.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
}
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
Text("\(bridgeModule.currentWorkoutRunTimeInSeconds)")
.font(Font.system(size: 100))
.font(WerkoutTheme.stat)
.foregroundColor(WerkoutTheme.accent)
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 100)
}

View File

@@ -9,25 +9,20 @@ import SwiftUI
struct WorkoutInfoView: View {
let workout: Workout
var body: some View {
VStack {
VStack(alignment: .leading, spacing: WerkoutTheme.sm) {
Text(workout.name)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title3)
.padding(.top
)
.font(WerkoutTheme.sectionTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
if let desc = workout.description {
Text(desc)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding(.top)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
//#Preview {
// WorkoutInfoView()
//}

View File

@@ -9,58 +9,47 @@ import SwiftUI
struct WorkoutOverviewView: View {
let workout: Workout
var body: some View {
VStack {
HStack {
VStack {
Text(workout.name)
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
Text(workout.description ?? "")
.frame(maxWidth: .infinity, alignment: .leading)
if let estimatedTime = workout.estimatedTime {
Text("Time: " + estimatedTime.asString(style: .abbreviated))
.frame(maxWidth: .infinity, alignment: .leading)
}
if let createdAt = workout.createdAt {
Text(createdAt, style: .date)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
VStack(alignment: .leading, spacing: WerkoutTheme.xs) {
Text(workout.name)
.font(WerkoutTheme.cardTitle)
.foregroundStyle(WerkoutTheme.textPrimary)
.lineLimit(1)
if let description = workout.description, !description.isEmpty {
Text(description)
.font(WerkoutTheme.bodyText)
.foregroundStyle(WerkoutTheme.textSecondary)
.lineLimit(2)
}
HStack(spacing: 12) {
if let estimatedTime = workout.estimatedTime {
Label(estimatedTime.asString(style: .abbreviated), systemImage: "clock")
}
if let exerciseCount = workout.exercise_count {
VStack {
Text("\(exerciseCount)")
.font(.body.bold())
Text("exercises")
.font(.footnote)
.foregroundColor(Color(uiColor: .systemGray2))
}
Label("\(exerciseCount) exercises", systemImage: "list.bullet")
}
if let muscles = workout.muscles, !muscles.isEmpty {
Label(muscleSummary(muscles), systemImage: "figure.strengthtraining.traditional")
}
}
if let muscles = workout.muscles,
muscles.joined(separator: ", ").count > 0{
Divider()
Text(muscles.joined(separator: ", "))
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let equipment = workout.equipment,
equipment.joined(separator: ", ").count > 0 {
Divider()
Text(equipment.joined(separator: ", "))
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
.font(WerkoutTheme.caption)
.foregroundStyle(WerkoutTheme.textMuted)
}
.padding()
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(2)
.frame(maxWidth: .infinity, alignment: .leading)
.werkoutCard()
}
private func muscleSummary(_ muscles: [String]) -> String {
guard let first = muscles.first else { return "" }
if muscles.count <= 2 {
return muscles.joined(separator: ", ")
}
return "\(first), \(muscles[1]) +\(muscles.count - 2)"
}
}