// // MainView.swift // Werkout_ios // // Created by Trey Tartt on 6/14/23. // import SwiftUI import AVKit struct WorkoutDetailView: View { @ObservedObject var viewModel: WorkoutDetailViewModel @State private 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? @ObservedObject var bridgeModule = BridgeModule.shared @Environment(\.dismiss) var dismiss @AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never @AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female" enum Sheet: Identifiable { case completedWorkout([String: Any]) var id: String { return "completedWorkoutSheet" } } @State private var workoutComplete: Sheet? @State private var workoutToPlan: Workout? @State private var showExecersizeInfo: Bool = false var body: some View { ZStack { WerkoutTheme.background.ignoresSafeArea() switch viewModel.status { case .loading: ProgressView() .tint(WerkoutTheme.accent) case .failed(let errorMessage): VStack(spacing: WerkoutTheme.md) { Text("Unable to load workout") .font(WerkoutTheme.sectionTitle) .foregroundStyle(WerkoutTheme.textPrimary) Text(errorMessage) .font(WerkoutTheme.bodyText) .multilineTextAlignment(.center) .foregroundStyle(WerkoutTheme.textSecondary) } .padding() case .showWorkout(let workout): VStack(spacing: 0) { if bridgeModule.isInWorkout { HStack { CountdownView( currentExerciseDuration: bridgeModule.currentWorkoutInfo.currentExercise?.duration, currentExerciseTimeLeft: bridgeModule.currentExerciseTimeLeft ) } .padding() .frame(maxWidth: .infinity) if phoneThotStyle != .off { PlayerView(player: $avPlayer) .frame(height: 220) .onAppear{ avPlayer.isMuted = true avPlayer.play() } .overlay(alignment: .bottomTrailing) { Button(action: { if let assetURL = ((avPlayer.currentItem?.asset) as? AVURLAsset)?.url, let currentExtercise = bridgeModule.currentWorkoutInfo.currentExercise, let otherVideoURL = VideoURLCreator.videoURL( thotStyle: VideoURLCreator.otherVideoType(forVideoURL: assetURL), gender: thotGenderOption, defaultVideoURLStr: currentExtercise.exercise.videoURL, exerciseName: currentExtercise.exercise.name, workout: bridgeModule.currentWorkoutInfo.workout) { updatePlayer(for: otherVideoURL) } }) { Image(systemName: "arrow.triangle.2.circlepath.camera.fill") .font(.title2) .padding(WerkoutTheme.sm) } .glassEffect(.regular.interactive()) .tint(WerkoutTheme.accent) .padding(WerkoutTheme.sm) .accessibilityLabel("Switch video style") .accessibilityHint("Toggles between alternate and default exercise videos") } .overlay(alignment: .bottomLeading) { Button(action: { showExecersizeInfo.toggle() }) { Image(systemName: "info.circle.fill") .font(.title2) .padding(WerkoutTheme.sm) } .glassEffect(.regular.interactive()) .tint(WerkoutTheme.accent) .padding(WerkoutTheme.sm) .accessibilityLabel(showExecersizeInfo ? "Hide exercise info" : "Show exercise info") .accessibilityHint("Shows exercise description and target muscles") } .padding([.top, .bottom]) .background(WerkoutTheme.background) } } if !bridgeModule.isInWorkout { InfoView(workout: workout) .padding(.bottom) } if bridgeModule.isInWorkout { WerkoutTheme.divider.frame(height: 0.5) HStack { Text("\(bridgeModule.currentWorkoutInfo.currentRound) of \(bridgeModule.currentWorkoutInfo.numberOfRoundsInCurrentSuperSet)") .font(.system(size: 17, weight: .bold, design: .monospaced)) .foregroundStyle(WerkoutTheme.accent) .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 10) CurrentWorkoutElapsedTimeView( currentWorkoutRunTimeInSeconds: bridgeModule.currentWorkoutRunTimeInSeconds ) .frame(maxWidth: .infinity, alignment: .center) Text(progressText) .font(.system(size: 17, weight: .bold, design: .monospaced)) .foregroundStyle(WerkoutTheme.textPrimary) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.trailing, 10) } .padding([.top, .bottom]) .background(WerkoutTheme.surfaceCard) } WerkoutTheme.divider.frame(height: 0.5) ExerciseListView( workout: workout, showExecersizeInfo: $showExecersizeInfo, isInWorkout: bridgeModule.isInWorkout, currentSupersetIndex: bridgeModule.currentWorkoutInfo.supersetIndex, currentExerciseIndex: bridgeModule.currentWorkoutInfo.exerciseIndex, allSupersetExecerciseIndex: bridgeModule.currentWorkoutInfo.allSupersetExecerciseIndex, currentExercise: bridgeModule.currentWorkoutInfo.currentExercise, currentWorkout: bridgeModule.currentWorkoutInfo.workout, goToExerciseAt: { supersetIndex, exerciseIndex in bridgeModule.currentWorkoutInfo.goToExerciseAt( supersetIndex: supersetIndex, exerciseIndex: exerciseIndex ) } ) .padding([.top, .bottom], 10) .background(WerkoutTheme.background) ActionsView(completedWorkout: { bridgeModule.completeWorkout() }, planWorkout: { workout in workoutToPlan = workout }, workout: workout, showAddToCalendar: viewModel.isPreview, startWorkoutAction: { startWorkout(workout: workout) }) .frame(height: 56) } .sheet(item: $workoutComplete) { item in switch item { case .completedWorkout(let data): CompletedWorkoutView(postData: data, workout: workout, completedWorkoutDismissed: { uploaded in if uploaded { bridgeModule.resetCurrentWorkout() dismiss() } }) } } .sheet(item: $workoutToPlan) { workout in PlanWorkoutView(workout: workout, addedPlannedWorkout: { dismiss() }) } } } .onChange(of: bridgeModule.currentWorkoutInfo.allSupersetExecerciseIndex) { _, _ in playVideos() } .onChange(of: bridgeModule.isInWorkout) { _, _ in playVideos() } .onAppear{ viewModel.load() playVideos() bridgeModule.completedWorkout = { if let workoutData = createWorkoutData() { workoutComplete = .completedWorkout(workoutData) } } } .onReceive(NotificationCenter.default.publisher( for: UIScene.willEnterForegroundNotification)) { _ in avPlayer.isMuted = true avPlayer.play() } .onDisappear { avPlayer.pause() bridgeModule.completedWorkout = nil } } func playVideos() { 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) { 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, let startTime = bridgeModule.workoutStartDate?.timeFormatForUpload, let endTime = bridgeModule.workoutEndDate?.timeFormatForUpload else { return nil } let postBody = [ "difficulty": 1, "workout_start_time": startTime, "workout_end_time": endTime, "workout": workoutid, "total_time": bridgeModule.currentWorkoutRunTimeInSeconds ] as [String : Any] return postBody } } struct WorkoutDetailView_Previews: PreviewProvider { static let workoutDetail = PreviewData.workout() static var previews: some View { WorkoutDetailView(viewModel: WorkoutDetailViewModel(workout: WorkoutDetailView_Previews.workoutDetail, status: .showWorkout(WorkoutDetailView_Previews.workoutDetail), isPreview: true)) } }