Fix 28 issues from deep audit and UI audit + redesign changes
Some checks failed
Apple Platform CI / smoke-and-tests (push) Has been cancelled
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:
@@ -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: {})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
//}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
//}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
//}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user