Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation

This commit is contained in:
Trey t
2026-02-11 12:54:40 -06:00
parent e40275e694
commit acce712261
77 changed files with 2940 additions and 765 deletions

View File

@@ -10,15 +10,16 @@ import AVFoundation
struct CreateExerciseActionsView: View {
@ObservedObject var workoutExercise: CreateWorkoutExercise
var superset: CreateWorkoutSuperSet
@ObservedObject var superset: CreateWorkoutSuperSet
var viewModel: WorkoutViewModel
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4") ?? URL(fileURLWithPath: "/dev/null"))
@State private var currentVideoURL: URL?
@State var videoExercise: Exercise? {
didSet {
if let viddd = self.videoExercise?.videoURL,
let url = URL(string: BaseURLs.currentBaseURL + viddd) {
self.avPlayer = AVPlayer(url: url)
updatePlayer(for: url)
}
}
}
@@ -37,6 +38,7 @@ struct CreateExerciseActionsView: View {
}, onDecrement: {
workoutExercise.decreaseReps()
})
.accessibilityLabel("Reps")
}
}
@@ -49,6 +51,7 @@ struct CreateExerciseActionsView: View {
}, onDecrement: {
workoutExercise.decreaseWeight()
})
.accessibilityLabel("Weight")
}
HStack {
@@ -66,6 +69,7 @@ struct CreateExerciseActionsView: View {
}, onDecrement: {
workoutExercise.decreaseDuration()
})
.accessibilityLabel("Duration")
}
HStack {
@@ -80,6 +84,8 @@ struct CreateExerciseActionsView: View {
.background(.blue)
.cornerRadius(Constants.buttonRadius)
.buttonStyle(BorderlessButtonStyle())
.accessibilityLabel("Preview exercise video")
.accessibilityHint("Opens a video preview for this exercise")
Spacer()
@@ -89,7 +95,6 @@ struct CreateExerciseActionsView: View {
superset
.deleteExerciseForChosenSuperset(exercise: workoutExercise)
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
}) {
Image(systemName: "trash.fill")
}
@@ -98,6 +103,8 @@ struct CreateExerciseActionsView: View {
.background(.red)
.cornerRadius(Constants.buttonRadius)
.buttonStyle(BorderlessButtonStyle())
.accessibilityLabel("Delete exercise")
.accessibilityHint("Removes this exercise from the superset")
Spacer()
}
@@ -109,6 +116,23 @@ struct CreateExerciseActionsView: View {
avPlayer.play()
}
}
.onDisappear {
avPlayer.pause()
}
}
private func updatePlayer(for url: URL) {
if currentVideoURL == url {
avPlayer.seek(to: .zero)
avPlayer.isMuted = true
avPlayer.play()
return
}
currentVideoURL = url
avPlayer = AVPlayer(url: url)
avPlayer.isMuted = true
avPlayer.play()
}
}

View File

@@ -6,6 +6,7 @@
//
import SwiftUI
import SharedCore
class CreateWorkoutExercise: ObservableObject, Identifiable {
let id = UUID()
@@ -49,7 +50,7 @@ class CreateWorkoutExercise: ObservableObject, Identifiable {
}
func decreaseWeight() {
self.weight -= 15
self.weight -= 5
if self.weight < 0 {
self.weight = 0
}
@@ -90,6 +91,8 @@ class WorkoutViewModel: ObservableObject {
@Published var superSets = [CreateWorkoutSuperSet]()
@Published var title = String()
@Published var description = String()
@Published var validationError: String?
@Published var isUploading = false
@Published var randomValueForUpdatingValue = 0
func increaseRandomNumberForUpdating() {
@@ -111,60 +114,82 @@ class WorkoutViewModel: ObservableObject {
}
func showRoundsError() {
validationError = "Each superset must have at least one round."
}
func showNoDurationOrReps() {
validationError = "Each exercise must have reps or duration."
}
func uploadWorkout() {
guard isUploading == false else {
return
}
validationError = nil
let normalizedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard normalizedTitle.isEmpty == false else {
validationError = "Workout title is required."
return
}
var supersets = [[String: Any]]()
var supersetOrder = 1
superSets.forEach({ superset in
if superset.numberOfRounds == 0 {
showRoundsError()
return
}
for (supersetOffset, superset) in superSets.enumerated() {
var supersetInfo = [String: Any]()
supersetInfo["name"] = ""
supersetInfo["rounds"] = superset.numberOfRounds
supersetInfo["order"] = supersetOrder
supersetInfo["order"] = supersetOffset + 1
var exercises = [[String: Any]]()
var exerciseOrder = 1
for exercise in superset.exercises {
if exercise.reps == 0 && exercise.duration == 0 {
showNoDurationOrReps()
return
}
for (exerciseOffset, exercise) in superset.exercises.enumerated() {
let item = ["id": exercise.exercise.id,
"reps": exercise.reps,
"weight": exercise.weight,
"duration": exercise.duration,
"order": exerciseOrder] as [String : Any]
"order": exerciseOffset + 1] as [String : Any]
exercises.append(item)
exerciseOrder += 1
}
supersetInfo["exercises"] = exercises
supersets.append(supersetInfo)
supersetOrder += 1
})
let uploadBody = ["name": title,
}
if supersets.isEmpty {
validationError = "Add at least one superset before uploading."
return
}
let issues = WorkoutValidation.validateSupersets(supersets)
if let issue = issues.first {
switch issue.code {
case "invalid_rounds":
showRoundsError()
case "invalid_exercise_payload":
showNoDurationOrReps()
default:
validationError = issue.message
}
return
}
let uploadBody = ["name": normalizedTitle,
"description": description,
"supersets": supersets] as [String : Any]
isUploading = true
CreateWorkoutFetchable(postData: uploadBody).fetch(completion: { result in
DispatchQueue.main.async {
switch result {
case .success(_):
self.superSets.removeAll()
self.title = ""
NotificationCenter.default.post(name: NSNotification.Name("CreatedNewWorkout"), object: nil, userInfo: nil)
case .failure(let failure):
print(failure)
}
self.isUploading = false
switch result {
case .success(_):
self.superSets.removeAll()
self.title = ""
self.description = ""
NotificationCenter.default.post(
name: AppNotifications.createdNewWorkout,
object: nil,
userInfo: nil
)
case .failure(let failure):
self.validationError = "Failed to upload workout: \(failure.localizedDescription)"
}
})
}

View File

@@ -11,6 +11,11 @@ struct CreateWorkoutMainView: View {
@StateObject var viewModel = WorkoutViewModel()
@State var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
@State private var showAddExercise = false
private var canSubmit: Bool {
viewModel.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false &&
viewModel.isUploading == false
}
var body: some View {
VStack {
@@ -28,7 +33,7 @@ struct CreateWorkoutMainView: View {
ScrollViewReader { proxy in
List() {
ForEach($viewModel.superSets, id: \.id) { superset in
ForEach(viewModel.superSets) { superset in
CreateWorkoutSupersetView(
selectedCreateWorkoutSuperSet: $selectedCreateWorkoutSuperSet,
showAddExercise: $showAddExercise,
@@ -38,7 +43,9 @@ struct CreateWorkoutMainView: View {
// after adding new exercise we have to scroll to the bottom
// where the new exercise is sooo keep this so we can scroll
// to id 999
Text("this is the bottom 🤷‍♂️")
Color.clear
.frame(height: 1)
.accessibilityHidden(true)
.id(999)
.listRowSeparator(.hidden)
}
@@ -57,7 +64,7 @@ struct CreateWorkoutMainView: View {
// if left or right auto add the other side
// with a recover in between b/c its
// eaiser to delete a recover than add one
if exercise.side != nil && exercise.side!.count > 0 {
if exercise.side?.isEmpty == false {
let exercises = DataStore.shared.allExercise?.filter({
$0.name == exercise.name
})
@@ -79,7 +86,6 @@ struct CreateWorkoutMainView: View {
}
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
selectedCreateWorkoutSuperSet = nil
})
}
@@ -95,11 +101,21 @@ struct CreateWorkoutMainView: View {
.cornerRadius(Constants.buttonRadius)
.padding()
.frame(maxWidth: .infinity)
.accessibilityLabel("Add superset")
.accessibilityHint("Adds a new superset section to this workout")
Divider()
Button("Done", action: {
Button(action: {
viewModel.uploadWorkout()
}, label: {
if viewModel.isUploading {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
} else {
Text("Done")
}
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
@@ -108,13 +124,23 @@ struct CreateWorkoutMainView: View {
.cornerRadius(Constants.buttonRadius)
.padding()
.frame(maxWidth: .infinity)
.disabled(viewModel.title.isEmpty)
.disabled(canSubmit == false)
.accessibilityLabel("Upload workout")
.accessibilityHint("Uploads this workout to your account")
}
.frame(height: 44)
Divider()
}
.background(Color(uiColor: .systemGray5))
.alert("Create Workout", isPresented: Binding<Bool>(
get: { viewModel.validationError != nil },
set: { _ in viewModel.validationError = nil }
)) {
Button("OK", role: .cancel) {}
} message: {
Text(viewModel.validationError ?? "")
}
}
}