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

@@ -11,7 +11,8 @@ import AVKit
struct ExerciseListView: View {
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
@ObservedObject var bridgeModule = BridgeModule.shared
@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 previewVideoURL: URL?
var workout: Workout
@Binding var showExecersizeInfo: Bool
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
@@ -24,15 +25,14 @@ struct ExerciseListView: View {
defaultVideoURLStr: self.videoExercise?.videoURL,
exerciseName: self.videoExercise?.name,
workout: bridgeModule.currentWorkoutInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.isMuted = true
avPlayer.play()
updatePreviewPlayer(for: videoURL)
}
}
}
var body: some View {
if let supersets = workout.supersets {
let supersets = workout.supersets?.sorted(by: { $0.order < $1.order }) ?? []
if supersets.isEmpty == false {
ScrollViewReader { proxy in
List() {
ForEach(supersets.indices, id: \.self) { supersetIndex in
@@ -40,58 +40,13 @@ struct ExerciseListView: View {
Section(content: {
ForEach(superset.exercises.indices, id: \.self) { exerciseIndex in
let supersetExecercise = superset.exercises[exerciseIndex]
let rowID = rowIdentifier(
supersetIndex: supersetIndex,
exerciseIndex: exerciseIndex,
exercise: supersetExecercise
)
VStack {
HStack {
if bridgeModule.isInWorkout &&
supersetIndex == bridgeModule.currentWorkoutInfo.supersetIndex &&
exerciseIndex == bridgeModule.currentWorkoutInfo.exerciseIndex {
Image(systemName: "figure.run")
.foregroundColor(Color("appColor"))
}
Text(supersetExecercise.exercise.extName)
Spacer()
if let reps = supersetExecercise.reps,
reps > 0 {
HStack {
Image(systemName: "number")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(reps)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.blue)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
.frame(alignment: .trailing)
}
if let duration = supersetExecercise.duration,
duration > 0 {
HStack {
Image(systemName: "stopwatch")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(duration)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.green)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
}
}
.padding(.trailing, -20)
.contentShape(Rectangle())
.onTapGesture {
Button(action: {
if bridgeModule.isInWorkout {
bridgeModule.currentWorkoutInfo.goToExerciseAt(
supersetIndex: supersetIndex,
@@ -99,7 +54,61 @@ struct ExerciseListView: View {
} else {
videoExercise = supersetExecercise.exercise
}
}
}, label: {
HStack {
if bridgeModule.isInWorkout &&
supersetIndex == bridgeModule.currentWorkoutInfo.supersetIndex &&
exerciseIndex == bridgeModule.currentWorkoutInfo.exerciseIndex {
Image(systemName: "figure.run")
.foregroundColor(Color("appColor"))
}
Text(supersetExecercise.exercise.extName)
Spacer()
if let reps = supersetExecercise.reps,
reps > 0 {
HStack {
Image(systemName: "number")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(reps)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.blue)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
.frame(alignment: .trailing)
}
if let duration = supersetExecercise.duration,
duration > 0 {
HStack {
Image(systemName: "stopwatch")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(duration)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.green)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
}
}
.padding(.trailing, -20)
.contentShape(Rectangle())
})
.buttonStyle(.plain)
.accessibilityLabel("Exercise \(supersetExecercise.exercise.extName)")
.accessibilityHint(bridgeModule.isInWorkout ? "Jump to this exercise in the workout" : "Preview exercise video")
if bridgeModule.isInWorkout &&
supersetIndex == bridgeModule.currentWorkoutInfo.supersetIndex &&
@@ -107,7 +116,7 @@ struct ExerciseListView: View {
showExecersizeInfo {
detailView(forExercise: supersetExecercise)
}
}.id(supersetExecercise.id)
}.id(rowID)
}
}, header: {
HStack {
@@ -131,7 +140,16 @@ struct ExerciseListView: View {
.onChange(of: bridgeModule.currentWorkoutInfo.allSupersetExecerciseIndex, perform: { newValue in
if let newCurrentExercise = bridgeModule.currentWorkoutInfo.currentExercise {
withAnimation {
proxy.scrollTo(newCurrentExercise.id, anchor: .top)
let currentSupersetIndex = bridgeModule.currentWorkoutInfo.supersetIndex
let currentExerciseIndex = bridgeModule.currentWorkoutInfo.exerciseIndex
proxy.scrollTo(
rowIdentifier(
supersetIndex: currentSupersetIndex,
exerciseIndex: currentExerciseIndex,
exercise: newCurrentExercise
),
anchor: .top
)
}
}
})
@@ -142,9 +160,36 @@ struct ExerciseListView: View {
avPlayer.play()
}
}
.onDisappear {
avPlayer.pause()
}
}
}
}
private func rowIdentifier(supersetIndex: Int, exerciseIndex: Int, exercise: SupersetExercise) -> String {
if let uniqueID = exercise.uniqueID, uniqueID.isEmpty == false {
return uniqueID
}
if let id = exercise.id {
return "exercise-\(id)"
}
return "superset-\(supersetIndex)-exercise-\(exerciseIndex)"
}
private func updatePreviewPlayer(for url: URL) {
if previewVideoURL == url {
avPlayer.seek(to: .zero)
avPlayer.isMuted = true
avPlayer.play()
return
}
previewVideoURL = url
avPlayer = AVPlayer(url: url)
avPlayer.isMuted = true
avPlayer.play()
}
func detailView(forExercise supersetExecercise: SupersetExercise) -> some View {
VStack {

View File

@@ -10,7 +10,8 @@ import AVKit
struct WorkoutDetailView: View {
@StateObject var viewModel: WorkoutDetailViewModel
@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?
@StateObject var bridgeModule = BridgeModule.shared
@Environment(\.dismiss) var dismiss
@@ -31,6 +32,16 @@ struct WorkoutDetailView: View {
switch viewModel.status {
case .loading:
Text("Loading")
case .failed(let errorMessage):
VStack(spacing: 16) {
Text("Unable to load workout")
.font(.headline)
Text(errorMessage)
.font(.footnote)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.padding()
case .showWorkout(let workout):
VStack(spacing: 0) {
if bridgeModule.isInWorkout {
@@ -59,9 +70,7 @@ struct WorkoutDetailView: View {
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentWorkoutInfo.workout) {
avPlayer = AVPlayer(url: otherVideoURL)
avPlayer.isMuted = true
avPlayer.play()
updatePlayer(for: otherVideoURL)
}
}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
@@ -72,6 +81,8 @@ struct WorkoutDetailView: View {
.cornerRadius(Constants.buttonRadius)
.frame(width: 160, height: 120)
.position(x: metrics.size.width - 22, y: metrics.size.height - 30)
.accessibilityLabel("Switch video style")
.accessibilityHint("Toggles between alternate and default exercise videos")
Button(action: {
showExecersizeInfo.toggle()
@@ -84,6 +95,8 @@ struct WorkoutDetailView: View {
.cornerRadius(Constants.buttonRadius)
.frame(width: 120, height: 120)
.position(x: 22, y: metrics.size.height - 30)
.accessibilityLabel(showExecersizeInfo ? "Hide exercise info" : "Show exercise info")
.accessibilityHint("Shows exercise description and target muscles")
}
}
.padding([.top, .bottom])
@@ -109,7 +122,7 @@ struct WorkoutDetailView: View {
CurrentWorkoutElapsedTimeView()
.frame(maxWidth: .infinity, alignment: .center)
Text("\(bridgeModule.currentWorkoutInfo.allSupersetExecerciseIndex+1)/\(bridgeModule.currentWorkoutInfo.workout?.allSupersetExecercise?.count ?? -99)")
Text(progressText)
.font(.title3)
.bold()
.frame(maxWidth: .infinity, alignment: .trailing)
@@ -163,18 +176,7 @@ struct WorkoutDetailView: View {
playVideos()
})
.onAppear{
if let currentExtercise = bridgeModule.currentWorkoutInfo.currentExercise {
if let videoURL = VideoURLCreator.videoURL(
thotStyle: phoneThotStyle,
gender: thotGenderOption,
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentWorkoutInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.isMuted = true
avPlayer.play()
}
}
playVideos()
bridgeModule.completedWorkout = {
if let workoutData = createWorkoutData() {
@@ -187,6 +189,10 @@ struct WorkoutDetailView: View {
avPlayer.isMuted = true
avPlayer.play()
}
.onDisappear {
avPlayer.pause()
bridgeModule.completedWorkout = nil
}
}
func playVideos() {
@@ -197,16 +203,38 @@ struct WorkoutDetailView: View {
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentWorkoutInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.isMuted = true
avPlayer.play()
updatePlayer(for: videoURL)
}
}
}
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()
}
func startWorkout(workout: Workout) {
bridgeModule.start(workout: workout)
}
private var progressText: String {
let totalExercises = bridgeModule.currentWorkoutInfo.workout?.allSupersetExecercise?.count ?? 0
guard totalExercises > 0 else {
return "0/0"
}
let current = min(totalExercises, max(1, bridgeModule.currentWorkoutInfo.allSupersetExecerciseIndex + 1))
return "\(current)/\(totalExercises)"
}
func createWorkoutData() -> [String:Any]? {
guard let workoutid = bridgeModule.currentWorkoutInfo.workout?.id,

View File

@@ -12,6 +12,7 @@ class WorkoutDetailViewModel: ObservableObject {
enum WorkoutDetailViewModelStatus {
case loading
case showWorkout(Workout)
case failed(String)
}
@Published var status: WorkoutDetailViewModelStatus
@@ -31,7 +32,9 @@ class WorkoutDetailViewModel: ObservableObject {
self.status = .showWorkout(model)
}
case .failure(let failure):
fatalError("failed \(failure.localizedDescription)")
DispatchQueue.main.async {
self.status = .failed("Failed to load workout details: \(failure.localizedDescription)")
}
}
})
}