// // AllWorkoutsView.swift // Werkout_ios // // Created by Trey Tartt on 6/15/23. // import Foundation import SwiftUI import HealthKit import SharedCore enum MainViewTypes: Int, CaseIterable { case AllWorkout = 0 case MyWorkouts var title: String { switch self { case .AllWorkout: return "All Workouts" case .MyWorkouts: return "Planned" } } } struct AllWorkoutsView: View { @State private var isUpdating = false private var workouts: [Workout]? { dataStore.allWorkouts?.sorted(by: { $0.createdAt ?? Date() < $1.createdAt ?? Date() }) } private var uniqueWorkoutUsers: [RegisteredUser]? { dataStore.workoutsUniqueUsers } let healthStore = HKHealthStore() var bridgeModule = BridgeModule.shared @State private var needsUpdating: Bool = true @ObservedObject var dataStore = DataStore.shared @ObservedObject var userStore = UserStore.shared @State private var showWorkoutDetail = false @State private var selectedWorkout: Workout? { didSet { bridgeModule.currentWorkoutInfo.workout = selectedWorkout } } @State private var selectedPlannedWorkout: Workout? { didSet { bridgeModule.currentWorkoutInfo.workout = selectedPlannedWorkout } } @State private var showLoginView = false @State private var selectedSegment: MainViewTypes = .AllWorkout @State private var selectedDate: Date = Date() private let runtimeReporter = RuntimeReporter.shared // MARK: - Hoisted filter state @State private var searchText = "" @State private var selectedMuscles: Set = [] @State private var selectedEquipment: Set = [] @State private var filteredRegisterdUser: RegisteredUser? @State private var currentSort: SortType? @State private var sortAscending: Bool = true let pub = NotificationCenter.default.publisher(for: AppNotifications.createdNewWorkout) private var hasActiveFilters: Bool { !selectedMuscles.isEmpty || !selectedEquipment.isEmpty || filteredRegisterdUser != nil } var body: some View { NavigationStack { ZStack { WerkoutTheme.background.ignoresSafeArea() if let workouts = workouts { VStack(spacing: 0) { // Active filter chips if hasActiveFilters { filterChipsRow } switch selectedSegment { case .AllWorkout: if isUpdating { ProgressView() .progressViewStyle(.circular) .tint(WerkoutTheme.accent) } AllWorkoutsListView(workouts: workouts, searchText: $searchText, selectedMuscles: $selectedMuscles, selectedEquipment: $selectedEquipment, filteredRegisterdUser: $filteredRegisterdUser, currentSort: $currentSort, sortAscending: $sortAscending, selectedWorkout: { workout in selectedWorkout = workout }, refresh: { self.needsUpdating = true maybeRefreshData() }) case .MyWorkouts: PlannedWorkoutView(workouts: userStore.plannedWorkouts, selectedPlannedWorkout: $selectedPlannedWorkout) } } } else { ProgressView() .progressViewStyle(.circular) .tint(WerkoutTheme.accent) } } .navigationTitle("Workouts") .searchable(text: $searchText, prompt: "Search workouts") .toolbar { ToolbarItem(placement: .principal) { Picker("View", selection: $selectedSegment) { ForEach(MainViewTypes.allCases, id: \.self) { viewType in Text(viewType.title).tag(viewType) } } .pickerStyle(.segmented) .frame(width: 200) } ToolbarItem(placement: .topBarTrailing) { HStack(spacing: WerkoutTheme.sm) { if bridgeModule.isInWorkout { Button(action: { selectedWorkout = bridgeModule.currentWorkoutInfo.workout }) { Image(systemName: "figure.strengthtraining.traditional") } .tint(WerkoutTheme.accent) } filterMenu } } } } .onAppear { authorizeHealthKit() maybeRefreshData() } .sheet(item: $selectedWorkout) { item in let isPreview = item.id == bridgeModule.currentWorkoutInfo.workout?.id let viewModel = WorkoutDetailViewModel(workout: item, isPreview: isPreview) WorkoutDetailView(viewModel: viewModel) } .sheet(item: $selectedPlannedWorkout) { item in let viewModel = WorkoutDetailViewModel(workout: item, isPreview: true) WorkoutDetailView(viewModel: viewModel) } .sheet(isPresented: $showLoginView) { LoginView(completion: { self.needsUpdating = true maybeRefreshData() }) .interactiveDismissDisabled() } .onReceive(pub) { _ in self.needsUpdating = true maybeRefreshData() } } // MARK: - Filter Menu private var filterMenu: some View { Menu { // Sort section Section("Sort") { ForEach(SortType.allCases, id: \.self) { sortType in Button(action: { if currentSort == sortType { sortAscending.toggle() } else { currentSort = sortType sortAscending = true } }) { Label { Text(sortType.rawValue) } icon: { if currentSort == sortType { Image(systemName: sortAscending ? "chevron.up" : "chevron.down") } } } } } // Creator filter if let users = uniqueWorkoutUsers, !users.isEmpty { Section("Creator") { ForEach(users, id: \.self) { user in Button(action: { filteredRegisterdUser = user }) { Label { Text("\(user.firstName ?? "") \(user.lastName ?? "")") } icon: { if filteredRegisterdUser == user { Image(systemName: "checkmark") } } } } Button("All Creators") { filteredRegisterdUser = nil } } } // Muscles filter if let muscles = DataStore.shared.allMuscles { Section("Muscles") { ForEach(muscles, id: \.id) { muscle in Button(action: { if selectedMuscles.contains(muscle.name) { selectedMuscles.remove(muscle.name) } else { selectedMuscles.insert(muscle.name) } }) { Label { Text(muscle.name) } icon: { if selectedMuscles.contains(muscle.name) { Image(systemName: "checkmark") } } } } } } // Equipment filter if let equipments = DataStore.shared.allEquipment { Section("Equipment") { ForEach(equipments, id: \.id) { equipment in Button(action: { if selectedEquipment.contains(equipment.name) { selectedEquipment.remove(equipment.name) } else { selectedEquipment.insert(equipment.name) } }) { Label { Text(equipment.name) } icon: { if selectedEquipment.contains(equipment.name) { Image(systemName: "checkmark") } } } } } } // Clear all if hasActiveFilters || currentSort != nil { Divider() Button(role: .destructive) { clearAllFilters() } label: { Label("Clear All Filters", systemImage: "xmark.circle") } } } label: { Image(systemName: hasActiveFilters ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") } .tint(hasActiveFilters ? WerkoutTheme.accent : nil) } // MARK: - Filter Chips Row private var filterChipsRow: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: WerkoutTheme.sm) { ForEach(Array(selectedMuscles), id: \.self) { muscle in FilterChip(label: muscle, color: WerkoutTheme.accent) { selectedMuscles.remove(muscle) } } ForEach(Array(selectedEquipment), id: \.self) { equipment in FilterChip(label: equipment, color: WerkoutTheme.textSecondary) { selectedEquipment.remove(equipment) } } if let user = filteredRegisterdUser { FilterChip(label: user.firstName ?? "User", color: WerkoutTheme.accentNeon) { filteredRegisterdUser = nil } } } .padding(.horizontal, WerkoutTheme.md) .padding(.vertical, WerkoutTheme.xs) } } // MARK: - Actions private func clearAllFilters() { selectedMuscles.removeAll() selectedEquipment.removeAll() filteredRegisterdUser = nil currentSort = nil sortAscending = true } func maybeRefreshData() { if userStore.token != nil { if userStore.plannedWorkouts.isEmpty { userStore.fetchPlannedWorkouts() } if needsUpdating { self.isUpdating = true dataStore.fetchAllData(completion: { DispatchQueue.main.async { self.isUpdating = false self.needsUpdating = false } }) } } else { isUpdating = false showLoginView = true } } func authorizeHealthKit() { let quantityTypes = [ HKObjectType.quantityType(forIdentifier: .heartRate), HKObjectType.quantityType(forIdentifier: .activeEnergyBurned), HKObjectType.quantityType(forIdentifier: .oxygenSaturation) ].compactMap { $0 } let healthKitTypes: Set = Set( quantityTypes + [ HKObjectType.activitySummaryType(), HKQuantityType.workoutType() ] ) 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"] ) } } } } struct AllWorkoutsView_Previews: PreviewProvider { static var previews: some View { AllWorkoutsView() } }