Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation
This commit is contained in:
@@ -19,7 +19,7 @@ struct AllWorkoutsListView: View {
|
||||
@Binding var uniqueWorkoutUsers: [RegisteredUser]?
|
||||
@State private var filteredRegisterdUser: RegisteredUser?
|
||||
|
||||
@State var workouts: [Workout]
|
||||
let workouts: [Workout]
|
||||
let selectedWorkout: ((Workout) -> Void)
|
||||
@State var filteredWorkouts = [Workout]()
|
||||
var refresh: (() -> Void)
|
||||
@@ -38,19 +38,23 @@ struct AllWorkoutsListView: View {
|
||||
uniqueWorkoutUsers: $uniqueWorkoutUsers,
|
||||
filteredRegisterdUser: $filteredRegisterdUser,
|
||||
filteredWorkouts: $filteredWorkouts,
|
||||
workouts: $workouts,
|
||||
workouts: .constant(workouts),
|
||||
currentSort: $currentSort)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 10) {
|
||||
ForEach(filteredWorkouts, id:\.id) { workout in
|
||||
WorkoutOverviewView(workout: workout)
|
||||
.padding([.leading, .trailing], 4)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedWorkout(workout)
|
||||
}
|
||||
Button(action: {
|
||||
selectedWorkout(workout)
|
||||
}, label: {
|
||||
WorkoutOverviewView(workout: workout)
|
||||
.padding([.leading, .trailing], 4)
|
||||
.contentShape(Rectangle())
|
||||
})
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Open \(workout.name)")
|
||||
.accessibilityHint("Shows workout details")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +75,9 @@ struct AllWorkoutsListView: View {
|
||||
.onAppear{
|
||||
filterWorkouts()
|
||||
}
|
||||
.onChange(of: workouts) { _ in
|
||||
filterWorkouts()
|
||||
}
|
||||
}
|
||||
|
||||
func filterWorkouts() {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import HealthKit
|
||||
import SharedCore
|
||||
|
||||
enum MainViewTypes: Int, CaseIterable {
|
||||
case AllWorkout = 0
|
||||
@@ -33,6 +34,7 @@ struct AllWorkoutsView: View {
|
||||
@State public var needsUpdating: Bool = true
|
||||
|
||||
@ObservedObject var dataStore = DataStore.shared
|
||||
@ObservedObject var userStore = UserStore.shared
|
||||
|
||||
@State private var showWorkoutDetail = false
|
||||
@State private var selectedWorkout: Workout? {
|
||||
@@ -50,8 +52,9 @@ struct AllWorkoutsView: View {
|
||||
@State private var showLoginView = false
|
||||
@State private var selectedSegment: MainViewTypes = .AllWorkout
|
||||
@State var selectedDate: Date = Date()
|
||||
private let runtimeReporter = RuntimeReporter.shared
|
||||
|
||||
let pub = NotificationCenter.default.publisher(for: NSNotification.Name("CreatedNewWorkout"))
|
||||
let pub = NotificationCenter.default.publisher(for: AppNotifications.createdNewWorkout)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -76,12 +79,12 @@ struct AllWorkoutsView: View {
|
||||
selectedWorkout = workout
|
||||
}, refresh: {
|
||||
self.needsUpdating = true
|
||||
maybeUpdateShit()
|
||||
maybeRefreshData()
|
||||
})
|
||||
|
||||
Divider()
|
||||
case .MyWorkouts:
|
||||
PlannedWorkoutView(workouts: UserStore.shared.plannedWorkouts,
|
||||
PlannedWorkoutView(workouts: userStore.plannedWorkouts,
|
||||
selectedPlannedWorkout: $selectedPlannedWorkout)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +96,7 @@ struct AllWorkoutsView: View {
|
||||
.onAppear{
|
||||
// UserStore.shared.logout()
|
||||
authorizeHealthKit()
|
||||
maybeUpdateShit()
|
||||
maybeRefreshData()
|
||||
}
|
||||
.sheet(item: $selectedWorkout) { item in
|
||||
let isPreview = item.id == bridgeModule.currentWorkoutInfo.workout?.id
|
||||
@@ -107,20 +110,20 @@ struct AllWorkoutsView: View {
|
||||
.sheet(isPresented: $showLoginView) {
|
||||
LoginView(completion: {
|
||||
self.needsUpdating = true
|
||||
maybeUpdateShit()
|
||||
maybeRefreshData()
|
||||
})
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
.onReceive(pub) { (output) in
|
||||
self.needsUpdating = true
|
||||
maybeUpdateShit()
|
||||
maybeRefreshData()
|
||||
}
|
||||
}
|
||||
|
||||
func maybeUpdateShit() {
|
||||
if UserStore.shared.token != nil{
|
||||
if UserStore.shared.plannedWorkouts.isEmpty {
|
||||
UserStore.shared.fetchPlannedWorkouts()
|
||||
func maybeRefreshData() {
|
||||
if userStore.token != nil{
|
||||
if userStore.plannedWorkouts.isEmpty {
|
||||
userStore.fetchPlannedWorkouts()
|
||||
}
|
||||
|
||||
if needsUpdating {
|
||||
@@ -128,6 +131,7 @@ struct AllWorkoutsView: View {
|
||||
dataStore.fetchAllData(completion: {
|
||||
DispatchQueue.main.async {
|
||||
guard let allWorkouts = dataStore.allWorkouts else {
|
||||
self.isUpdating = false
|
||||
return
|
||||
}
|
||||
self.workouts = allWorkouts.sorted(by: {
|
||||
@@ -140,22 +144,31 @@ struct AllWorkoutsView: View {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
isUpdating = false
|
||||
showLoginView = true
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeHealthKit() {
|
||||
let healthKitTypes: Set = [
|
||||
HKObjectType.quantityType(forIdentifier: .heartRate)!,
|
||||
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!,
|
||||
HKObjectType.activitySummaryType(),
|
||||
HKQuantityType.workoutType()
|
||||
]
|
||||
let quantityTypes = [
|
||||
HKObjectType.quantityType(forIdentifier: .heartRate),
|
||||
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
|
||||
HKObjectType.quantityType(forIdentifier: .oxygenSaturation)
|
||||
].compactMap { $0 }
|
||||
|
||||
let healthKitTypes: Set<HKObjectType> = Set(
|
||||
quantityTypes + [
|
||||
HKObjectType.activitySummaryType(),
|
||||
HKQuantityType.workoutType()
|
||||
]
|
||||
)
|
||||
|
||||
healthStore.requestAuthorization(toShare: nil, read: healthKitTypes) { (succ, error) in
|
||||
if !succ {
|
||||
// fatalError("Error requesting authorization from health store: \(String(describing: error)))")
|
||||
healthStore.requestAuthorization(toShare: nil, read: healthKitTypes) { (success, error) in
|
||||
if success == false {
|
||||
runtimeReporter.recordWarning(
|
||||
"HealthKit authorization request did not succeed",
|
||||
metadata: ["error": error?.localizedDescription ?? "unknown"]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import HealthKit
|
||||
import SharedCore
|
||||
|
||||
struct CompletedWorkoutView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
@@ -15,6 +16,9 @@ struct CompletedWorkoutView: View {
|
||||
@State var notes: String = ""
|
||||
@State var isUploading: Bool = false
|
||||
@State var gettingHealthKitData: Bool = false
|
||||
@State private var hasError = false
|
||||
@State private var errorMessage = ""
|
||||
private let runtimeReporter = RuntimeReporter.shared
|
||||
|
||||
var postData: [String: Any]
|
||||
let healthKitHelper = HealthKitHelper()
|
||||
@@ -60,7 +64,6 @@ struct CompletedWorkoutView: View {
|
||||
Spacer()
|
||||
|
||||
Button("Upload", action: {
|
||||
isUploading = true
|
||||
upload(postBody: postData)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -70,11 +73,14 @@ struct CompletedWorkoutView: View {
|
||||
.cornerRadius(Constants.buttonRadius)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(isUploading || gettingHealthKitData)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
}
|
||||
.onAppear{
|
||||
bridgeModule.sendWorkoutCompleteToWatch()
|
||||
.alert("Upload Failed", isPresented: $hasError) {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
// .onChange(of: bridgeModule.healthKitUUID, perform: { healthKitUUID in
|
||||
// if let healthKitUUID = healthKitUUID {
|
||||
@@ -92,6 +98,11 @@ struct CompletedWorkoutView: View {
|
||||
}
|
||||
|
||||
func upload(postBody: [String: Any]) {
|
||||
guard isUploading == false else {
|
||||
return
|
||||
}
|
||||
isUploading = true
|
||||
|
||||
var _postBody = postBody
|
||||
_postBody["difficulty"] = difficulty
|
||||
_postBody["notes"] = notes
|
||||
@@ -103,6 +114,7 @@ struct CompletedWorkoutView: View {
|
||||
switch result {
|
||||
case .success(_):
|
||||
DispatchQueue.main.async {
|
||||
self.isUploading = false
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
dismiss()
|
||||
completedWorkoutDismissed?(true)
|
||||
@@ -110,8 +122,13 @@ struct CompletedWorkoutView: View {
|
||||
case .failure(let failure):
|
||||
DispatchQueue.main.async {
|
||||
self.isUploading = false
|
||||
self.errorMessage = failure.localizedDescription
|
||||
self.hasError = true
|
||||
}
|
||||
print(failure)
|
||||
runtimeReporter.recordError(
|
||||
"Completed workout upload failed",
|
||||
metadata: ["error": failure.localizedDescription]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ import AVKit
|
||||
|
||||
struct ExternalWorkoutDetailView: View {
|
||||
@StateObject 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 smallAVPlayer = 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 var smallAVPlayer = 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 private var currentSmallVideoURL: URL?
|
||||
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
|
||||
@AppStorage(Constants.extShowNextVideo) private var extShowNextVideo: Bool = false
|
||||
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
|
||||
@@ -49,8 +51,8 @@ struct ExternalWorkoutDetailView: View {
|
||||
.frame(width: metrics.size.width * 0.2,
|
||||
height: metrics.size.height * 0.2)
|
||||
.onAppear{
|
||||
avPlayer.isMuted = true
|
||||
avPlayer.play()
|
||||
smallAVPlayer.isMuted = true
|
||||
smallAVPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +107,10 @@ struct ExternalWorkoutDetailView: View {
|
||||
avPlayer.play()
|
||||
smallAVPlayer.play()
|
||||
}
|
||||
.onDisappear {
|
||||
avPlayer.pause()
|
||||
smallAVPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
func playVideos() {
|
||||
@@ -115,8 +121,7 @@ struct ExternalWorkoutDetailView: View {
|
||||
defaultVideoURLStr: currentExtercise.exercise.videoURL,
|
||||
exerciseName: currentExtercise.exercise.name,
|
||||
workout: bridgeModule.currentWorkoutInfo.workout) {
|
||||
avPlayer = AVPlayer(url: videoURL)
|
||||
avPlayer.play()
|
||||
updateMainPlayer(for: videoURL)
|
||||
}
|
||||
|
||||
if let smallVideoURL = VideoURLCreator.videoURL(
|
||||
@@ -126,11 +131,34 @@ struct ExternalWorkoutDetailView: View {
|
||||
exerciseName: BridgeModule.shared.currentWorkoutInfo.nextExerciseInfo?.exercise.name,
|
||||
workout: bridgeModule.currentWorkoutInfo.workout),
|
||||
extShowNextVideo {
|
||||
smallAVPlayer = AVPlayer(url: smallVideoURL)
|
||||
smallAVPlayer.play()
|
||||
updateSmallPlayer(for: smallVideoURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMainPlayer(for url: URL) {
|
||||
if currentVideoURL == url {
|
||||
avPlayer.seek(to: .zero)
|
||||
avPlayer.play()
|
||||
return
|
||||
}
|
||||
|
||||
currentVideoURL = url
|
||||
avPlayer = AVPlayer(url: url)
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
private func updateSmallPlayer(for url: URL) {
|
||||
if currentSmallVideoURL == url {
|
||||
smallAVPlayer.seek(to: .zero)
|
||||
smallAVPlayer.play()
|
||||
return
|
||||
}
|
||||
|
||||
currentSmallVideoURL = url
|
||||
smallAVPlayer = AVPlayer(url: url)
|
||||
smallAVPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
//struct ExternalWorkoutDetailView_Previews: PreviewProvider {
|
||||
|
||||
@@ -12,7 +12,7 @@ struct LoginView: View {
|
||||
@State var password: String = ""
|
||||
@Environment(\.dismiss) var dismiss
|
||||
let completion: (() -> Void)
|
||||
@State var doingNetworkShit: Bool = false
|
||||
@State var isLoggingIn: Bool = false
|
||||
|
||||
@State var errorTitle = ""
|
||||
@State var errorMessage = ""
|
||||
@@ -23,13 +23,17 @@ struct LoginView: View {
|
||||
VStack {
|
||||
TextField("Email", text: $email)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.keyboardType(.emailAddress)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 25)
|
||||
.foregroundStyle(.black)
|
||||
.accessibilityLabel("Email")
|
||||
.submitLabel(.next)
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.password)
|
||||
@@ -37,10 +41,13 @@ struct LoginView: View {
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.padding(.horizontal)
|
||||
.autocapitalization(.none)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.foregroundStyle(.black)
|
||||
.accessibilityLabel("Password")
|
||||
.submitLabel(.go)
|
||||
|
||||
if doingNetworkShit {
|
||||
if isLoggingIn {
|
||||
ProgressView("Logging In")
|
||||
.padding()
|
||||
.foregroundColor(.white)
|
||||
@@ -58,6 +65,8 @@ struct LoginView: View {
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(password.isEmpty || email.isEmpty)
|
||||
.accessibilityLabel("Log in")
|
||||
.accessibilityHint("Logs in using the entered email and password")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -68,11 +77,12 @@ struct LoginView: View {
|
||||
.resizable()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.scaledToFill()
|
||||
.accessibilityHidden(true)
|
||||
)
|
||||
.alert(errorTitle, isPresented: $hasError, actions: {
|
||||
|
||||
}, message: {
|
||||
|
||||
Text(errorMessage)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,9 +91,9 @@ struct LoginView: View {
|
||||
"email": email,
|
||||
"password": password
|
||||
]
|
||||
doingNetworkShit = true
|
||||
isLoggingIn = true
|
||||
UserStore.shared.login(postData: postData, completion: { success in
|
||||
doingNetworkShit = false
|
||||
isLoggingIn = false
|
||||
if success {
|
||||
completion()
|
||||
dismiss()
|
||||
@@ -103,4 +113,3 @@ struct LoginView_Previews: PreviewProvider {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import SwiftUI
|
||||
|
||||
struct PlanWorkoutView: View {
|
||||
@State var selectedDate = Date()
|
||||
@State private var hasError = false
|
||||
@State private var errorMessage = ""
|
||||
let workout: Workout
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var addedPlannedWorkout: (() -> Void)?
|
||||
@@ -48,6 +50,8 @@ struct PlanWorkoutView: View {
|
||||
.background(.red)
|
||||
.cornerRadius(Constants.buttonRadius)
|
||||
.padding()
|
||||
.accessibilityLabel("Cancel planning")
|
||||
.accessibilityHint("Closes this screen without planning a workout")
|
||||
|
||||
Button(action: {
|
||||
planWorkout()
|
||||
@@ -62,11 +66,18 @@ struct PlanWorkoutView: View {
|
||||
.background(.yellow)
|
||||
.cornerRadius(Constants.buttonRadius)
|
||||
.padding()
|
||||
.accessibilityLabel("Plan workout")
|
||||
.accessibilityHint("Adds this workout to your selected date")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.alert("Unable to Plan Workout", isPresented: $hasError) {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func planWorkout() {
|
||||
@@ -78,11 +89,16 @@ struct PlanWorkoutView: View {
|
||||
PlanWorkoutFetchable(postData: postData).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
UserStore.shared.fetchPlannedWorkouts()
|
||||
dismiss()
|
||||
addedPlannedWorkout?()
|
||||
case .failure(_):
|
||||
fatalError("shit broke")
|
||||
DispatchQueue.main.async {
|
||||
UserStore.shared.fetchPlannedWorkouts()
|
||||
dismiss()
|
||||
addedPlannedWorkout?()
|
||||
}
|
||||
case .failure(let failure):
|
||||
DispatchQueue.main.async {
|
||||
errorMessage = failure.localizedDescription
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ struct WorkoutHistoryView: View {
|
||||
HStack {
|
||||
VStack {
|
||||
if let date = completedWorkout.workoutStartTime.dateFromServerDate {
|
||||
Text(DateFormatter().shortMonthSymbols[date.get(.month) - 1])
|
||||
Text(date.monthString)
|
||||
|
||||
Text("\(date.get(.day))")
|
||||
Text("\(date.get(.hour))")
|
||||
|
||||
Reference in New Issue
Block a user