From 1997abeff68136d08307a24770f8ae38c478f682 Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 28 Jul 2023 11:15:13 -0500 Subject: [PATCH] WIP --- Werkout_ios/APIModels/RegisteredUser.swift | 2 +- Werkout_ios/APIModels/Superset.swift | 2 + Werkout_ios/APIModels/Workout.swift | 21 +++ Werkout_ios/BaseURLs.swift | 2 +- Werkout_ios/CurrentWorkoutInfo.swift | 17 ++ Werkout_ios/DataStore.swift | 12 +- .../AllWorkouts/AllWorkoutsListView.swift | 159 ++++++++++++++---- .../Views/AllWorkouts/AllWorkoutsView.swift | 32 ++-- .../AllWorkouts/WorkoutOverviewView.swift | 11 ++ .../Views/ExternalWorkoutDetailView.swift | 92 +++++----- .../WorkoutDetail/ExerciseListView.swift | 6 + .../Views/WorkoutDetail/InfoView.swift | 8 + .../WorkoutDetail/WorkoutDetailView.swift | 1 - 13 files changed, 264 insertions(+), 101 deletions(-) diff --git a/Werkout_ios/APIModels/RegisteredUser.swift b/Werkout_ios/APIModels/RegisteredUser.swift index 5b93ae3..4ff56f7 100644 --- a/Werkout_ios/APIModels/RegisteredUser.swift +++ b/Werkout_ios/APIModels/RegisteredUser.swift @@ -7,7 +7,7 @@ import Foundation -struct RegisteredUser: Codable { +struct RegisteredUser: Codable, Hashable { let id: Int let firstName, lastName, image: String? let nickName: String? diff --git a/Werkout_ios/APIModels/Superset.swift b/Werkout_ios/APIModels/Superset.swift index 60db92b..dc95cd7 100644 --- a/Werkout_ios/APIModels/Superset.swift +++ b/Werkout_ios/APIModels/Superset.swift @@ -12,12 +12,14 @@ struct Superset: Codable, Identifiable, Hashable { let exercises: [SupersetExercise] let createdAt, updatedAt, name: String? let rounds, order, workout: Int + let estimatedTime: Double? enum CodingKeys: String, CodingKey { case id, exercises case createdAt = "created_at" case updatedAt = "updated_at" case name, rounds, order, workout + case estimatedTime = "estimated_time" } public func hash(into hasher: inout Hasher) { diff --git a/Werkout_ios/APIModels/Workout.swift b/Werkout_ios/APIModels/Workout.swift index 804327e..7621521 100644 --- a/Werkout_ios/APIModels/Workout.swift +++ b/Werkout_ios/APIModels/Workout.swift @@ -21,6 +21,9 @@ struct Workout: Codable, Identifiable, Equatable { let muscles: [String]? let equipment: [String]? let exercise_count: Int? + let createdAt: Date? + let estimatedTime: Double? + let allSupersetExecercise: [SupersetExercise]? enum CodingKeys: String, CodingKey { case id, name, description, supersets, exercise_count, muscles, equipment @@ -28,6 +31,9 @@ struct Workout: Codable, Identifiable, Equatable { case maleVideos = "male_videos" case femaleVideos = "female_videos" case bothVideos = "both_videos" + case createdAt = "created_at" + case estimatedTime = "estimated_time" + case allSupersetExecercise = "all_superset_exercise" } init(from decoder: Decoder) throws { @@ -45,5 +51,20 @@ struct Workout: Codable, Identifiable, Equatable { self.equipment = try container.decodeIfPresent([String].self, forKey: .equipment) self.muscles = try container.decodeIfPresent([String].self, forKey: .muscles) self.exercise_count = try container.decodeIfPresent(Int.self, forKey: .exercise_count) + + let createdAtStr = try container.decodeIfPresent(String.self, forKey: .createdAt) + if let createdAtStr = createdAtStr { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + let date = formatter.date(from: createdAtStr) + self.createdAt = date + } else { + self.createdAt = nil + } + + self.estimatedTime = try container.decodeIfPresent(Double.self, forKey: .estimatedTime) + + allSupersetExecercise = try container.decodeIfPresent([SupersetExercise].self, forKey: .allSupersetExecercise) } } diff --git a/Werkout_ios/BaseURLs.swift b/Werkout_ios/BaseURLs.swift index c43c8e6..537f209 100644 --- a/Werkout_ios/BaseURLs.swift +++ b/Werkout_ios/BaseURLs.swift @@ -12,6 +12,6 @@ enum BaseURLs: String { case dev = "https://dev.werkout.fitness" static var currentBaseURL: String { - return BaseURLs.dev.rawValue + return BaseURLs.local.rawValue } } diff --git a/Werkout_ios/CurrentWorkoutInfo.swift b/Werkout_ios/CurrentWorkoutInfo.swift index a619d85..f4d50b0 100644 --- a/Werkout_ios/CurrentWorkoutInfo.swift +++ b/Werkout_ios/CurrentWorkoutInfo.swift @@ -14,6 +14,7 @@ class CurrentWorkoutInfo { var complete: (() -> Void)? var currentRound = 1 + var allSupersetExecerciseIndex = 0 var superset: [Superset] { return workout?.supersets?.sorted(by: { $0.order < $1.order }) ?? [Superset]() @@ -57,6 +58,7 @@ class CurrentWorkoutInfo { let superset = supersets[supersetIndex] let exercise = superset.exercises[exerciseIndex] + allSupersetExecerciseIndex += 1 return exercise } @@ -84,6 +86,12 @@ class CurrentWorkoutInfo { let superset = supersets[supersetIndex] let exercise = superset.exercises[exerciseIndex] + + allSupersetExecerciseIndex -= 1 + if allSupersetExecerciseIndex < 0 { + allSupersetExecerciseIndex = 0 + } + return exercise } @@ -96,12 +104,21 @@ class CurrentWorkoutInfo { supersetIndex = 0 exerciseIndex = 0 currentRound = 1 + allSupersetExecerciseIndex = 0 self.workout = nil } func goToWorkoutAt(supersetIndex: Int, exerciseIndex: Int) -> SupersetExercise? { self.supersetIndex = supersetIndex self.exerciseIndex = exerciseIndex + self.currentRound = 1 + + let currentExercise = currentExercise + if let firstIdx = workout?.allSupersetExecercise?.firstIndex(where: { + $0.exercise.id == currentExercise?.exercise.id + }) { + allSupersetExecerciseIndex = firstIdx + } return currentExercise } diff --git a/Werkout_ios/DataStore.swift b/Werkout_ios/DataStore.swift index ab4b7e8..07e5979 100644 --- a/Werkout_ios/DataStore.swift +++ b/Werkout_ios/DataStore.swift @@ -25,7 +25,16 @@ class DataStore: ObservableObject { private let fetchAllDataQueue = DispatchGroup() - public func fetchAllData() { + public var workoutsUniqueUsers: [RegisteredUser]? { + guard let workouts = allWorkouts else { + return nil + } + + let users = workouts.compactMap({ $0.registeredUser }) + return Array(Set(users)) + } + + public func fetchAllData(completion: @escaping (() -> Void)) { status = .loading fetchAllDataQueue.enter() @@ -35,6 +44,7 @@ class DataStore: ObservableObject { fetchAllDataQueue.notify(queue: .main) { self.status = .idle + completion() } AllWorkoutFetchable().fetch(completion: { result in diff --git a/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift b/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift index 1c50f3a..7545781 100644 --- a/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift +++ b/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift @@ -8,36 +8,27 @@ import SwiftUI struct AllWorkoutsListView: View { - @State var searchString: String = "" - let workouts: [Workout] - - let selectedWorkout: ((Workout) -> Void) - - var filteredWorkouts: [Workout] { - if !searchString.isEmpty, searchString.count > 0 { - return workouts.filter({ - if $0.name.lowercased().contains(searchString.lowercased()) { - return true - } - if let equipment = $0.equipment?.joined(separator: "").lowercased(), - equipment.contains(searchString.lowercased()) { - return true - } - if let muscles = $0.muscles?.joined(separator: "").lowercased(), - muscles.contains(searchString.lowercased()) { - return true - } - - return false - }) - } else { - return workouts - } + enum SortType: String, CaseIterable { + case name = "Name" + case date = "Date" } + + @State var searchString: String = "" + @Binding var uniqueWorkoutUsers: [RegisteredUser]? + @State private var filteredRegisterdUser: RegisteredUser? + + let workouts: [Workout] + let selectedWorkout: ((Workout) -> Void) + @State var filteredWorkouts = [Workout]() var refresh: (() -> Void) + @State var currentSort: SortType? var body: some View { VStack { + if let filteredRegisterdUser = filteredRegisterdUser { + Text((filteredRegisterdUser.firstName ?? "NA") + "'s Workouts") + } + ScrollView { LazyVStack(spacing: 20) { ForEach(filteredWorkouts, id:\.id) { workout in @@ -51,24 +42,120 @@ struct AllWorkoutsListView: View { } } .refreshable { - refresh() + refresh() } - TextField("Filter" ,text: $searchString) - .padding() - .textFieldStyle(OvalTextFieldStyle()) + HStack { + TextField("Filter" ,text: $searchString) + .padding() + .textFieldStyle(OvalTextFieldStyle()) + + if let uniqueWorkoutUsers = uniqueWorkoutUsers { + Menu(content: { + ForEach(uniqueWorkoutUsers, id: \.self) { index in + Button(action: { + filteredRegisterdUser = index + filteredWorkouts = filterWorkouts() + }, label: { + Text((index.firstName ?? "") + " -" + (index.lastName ?? "")) + }) + } + + Button(action: { + filteredRegisterdUser = nil + filteredWorkouts = filterWorkouts() + }, label: { + Text("All") + }) + + }, label: { + Image(systemName: filteredRegisterdUser == nil ? "person.2" : "person.2.fill") + .padding(.trailing) + }) + } + + Menu(content: { + ForEach(SortType.allCases, id: \.self) { index in + Button(action: { + sortWorkouts(sortType: index) + }, label: { + Text(index.rawValue) + }) + } + }, label: { + Image(systemName: "list.number") + .padding(.trailing) + }) + } } + .onChange(of: searchString) { newValue in + filteredWorkouts = filterWorkouts() + } + .onAppear{ + filteredWorkouts = filterWorkouts() + } + } + + func sortWorkouts(sortType: SortType) { + if currentSort == sortType { + filteredWorkouts = filteredWorkouts.reversed() + return + } + switch sortType { + case .name: + filteredWorkouts = filteredWorkouts.sorted(by: { + $0.name < $1.name + }) + case .date: + filteredWorkouts = filteredWorkouts.sorted(by: { + $0.createdAt ?? Date() < $1.createdAt ?? Date() + }) + } + currentSort = sortType + } + + func filterWorkouts() -> [Workout] { + var matchingWorkouts = [Workout]() + + if (!searchString.isEmpty && searchString.count > 0) { + matchingWorkouts = workouts.filter({ + if $0.name.lowercased().contains(searchString.lowercased()) { + return true + } + + if let equipment = $0.equipment?.joined(separator: "").lowercased(), + equipment.contains(searchString.lowercased()) { + return true + } + + if let muscles = $0.muscles?.joined(separator: "").lowercased(), + muscles.contains(searchString.lowercased()) { + return true + } + + return false + }) + } + + if matchingWorkouts.isEmpty { + matchingWorkouts.append(contentsOf: workouts) + } + + if let filteredRegisterdUser = filteredRegisterdUser { + matchingWorkouts = matchingWorkouts.filter({ + $0.registeredUser == filteredRegisterdUser + }) + } + + return matchingWorkouts } } struct AllWorkoutsListView_Previews: PreviewProvider { static var previews: some View { - AllWorkoutsListView(workouts: PreviewData.allWorkouts(), - selectedWorkout: { workout in - - }, - refresh: { - - }) + AllWorkoutsListView(uniqueWorkoutUsers: .constant([]), + workouts: PreviewData.allWorkouts(), + selectedWorkout: { workout in }, + refresh: { }) } } diff --git a/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift b/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift index e196c8c..705ea6f 100644 --- a/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift +++ b/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift @@ -24,9 +24,10 @@ enum MainViewTypes: Int, CaseIterable { } struct AllWorkoutsView: View { - @State var isUpdating = false @State var workouts: [Workout]? + @State var uniqueWorkoutUsers: [RegisteredUser]? + var bridgeModule = BridgeModule.shared @State public var needsUpdating: Bool = true @@ -61,6 +62,7 @@ struct AllWorkoutsView: View { selectedWorkout = bridgeModule.currentExerciseInfo.workout }) + switch selectedSegment { case .AllWorkout: if isUpdating { @@ -68,7 +70,9 @@ struct AllWorkoutsView: View { .progressViewStyle(.circular) } - AllWorkoutsListView(workouts: workouts, selectedWorkout: { workout in + AllWorkoutsListView(uniqueWorkoutUsers: $uniqueWorkoutUsers, + workouts: workouts, + selectedWorkout: { workout in selectedWorkout = workout }, refresh: { self.needsUpdating = true @@ -155,21 +159,19 @@ struct AllWorkoutsView: View { if needsUpdating { self.isUpdating = true - dataStore.fetchAllData() - - AllWorkoutFetchable().fetch(completion: { result in - needsUpdating = false - switch result { - case .success(let model): - DispatchQueue.main.async { - self.workouts = model - self.isUpdating = false - } - case .failure(_): - DispatchQueue.main.async { - self.isUpdating = false + dataStore.fetchAllData(completion: { + DispatchQueue.main.async { + guard let allWorkouts = dataStore.allWorkouts else { + return } + self.workouts = allWorkouts.sorted(by: { + $0.createdAt ?? Date() < $1.createdAt ?? Date() + }) + self.isUpdating = false + self.uniqueWorkoutUsers = dataStore.workoutsUniqueUsers } + + self.isUpdating = false }) } } else { diff --git a/Werkout_ios/Views/AllWorkouts/WorkoutOverviewView.swift b/Werkout_ios/Views/AllWorkouts/WorkoutOverviewView.swift index 4cb5368..627df58 100644 --- a/Werkout_ios/Views/AllWorkouts/WorkoutOverviewView.swift +++ b/Werkout_ios/Views/AllWorkouts/WorkoutOverviewView.swift @@ -19,6 +19,17 @@ struct WorkoutOverviewView: View { Text(workout.description ?? "") .frame(maxWidth: .infinity, alignment: .leading) + + if let estimatedTime = workout.estimatedTime { + Text("Time: " + estimatedTime.asString(style: .abbreviated)) + .frame(maxWidth: .infinity, alignment: .leading) + } + + if let createdAt = workout.createdAt { + Text(createdAt, style: .date) + .font(.footnote) + .frame(maxWidth: .infinity, alignment: .leading) + } } if let exerciseCount = workout.exercise_count { VStack { diff --git a/Werkout_ios/Views/ExternalWorkoutDetailView.swift b/Werkout_ios/Views/ExternalWorkoutDetailView.swift index a550ea7..d34a96f 100644 --- a/Werkout_ios/Views/ExternalWorkoutDetailView.swift +++ b/Werkout_ios/Views/ExternalWorkoutDetailView.swift @@ -17,8 +17,7 @@ struct ExternalWorkoutDetailView: View { var body: some View { ZStack { - if let workout = bridgeModule.currentExerciseInfo.workout, - let exercise = bridgeModule.currentExerciseInfo.currentExercise { + if let workout = bridgeModule.currentExerciseInfo.workout { GeometryReader { metrics in VStack { HStack { @@ -32,7 +31,7 @@ struct ExternalWorkoutDetailView: View { VStack { ExtExerciseList(workout: workout, - currentExercise: exercise) + allSupersetExecerciseIndex: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex) if let currentExercisePositionString = bridgeModule.currentExercisePositionString { Text(currentExercisePositionString) @@ -153,63 +152,64 @@ struct TitleView: View { struct ExtExerciseList: View { var workout: Workout - var currentExercise: SupersetExercise + var allSupersetExecerciseIndex: Int var body: some View { - if let supersets = workout.supersets { - ScrollViewReader { proxy in - List() { - ForEach(supersets.indices, id: \.self) { supersetIndex in - let superset = supersets[supersetIndex] - - Section(content: { - ForEach(superset.exercises.indices, id: \.self) { exerciseIndex in - let supersetExecercise = superset.exercises[exerciseIndex] - - HStack { - if supersetExecercise.id == currentExercise.id { - Image(systemName: "checkmark") - .foregroundColor(.green) - .font(Font.system(size: 55)) - .minimumScaleFactor(0.01) - .lineLimit(1) - .foregroundColor(.green) - } - - Text(supersetExecercise.exercise.name) + if let allSupersetExecercise = workout.allSupersetExecercise { + ZStack { + ScrollViewReader { proxy in + List() { + ForEach(allSupersetExecercise.indices, id: \.self) { supersetExecerciseIdx in + let supersetExecercise = allSupersetExecercise[supersetExecerciseIdx] + HStack { + if supersetExecerciseIdx == allSupersetExecerciseIndex { + Image(systemName: "checkmark") + .foregroundColor(.green) .font(Font.system(size: 55)) .minimumScaleFactor(0.01) - .lineLimit(3) - .padding() - - Spacer() + .lineLimit(1) + .foregroundColor(.green) } - .id(supersetExecercise.id) - } - }, header: { - HStack { - Text(superset.name ?? "--") + + Text(supersetExecercise.exercise.name) .font(Font.system(size: 55)) .minimumScaleFactor(0.01) .lineLimit(3) .padding() Spacer() - Text("\(superset.rounds) rounds") - .font(Font.system(size: 55)) - .minimumScaleFactor(0.01) - .lineLimit(3) - .padding() } - }) + .id(supersetExecerciseIdx) + } } + .onChange(of: allSupersetExecerciseIndex, perform: { newValue in + withAnimation { + proxy.scrollTo(allSupersetExecerciseIndex, anchor: .top) + } + }) + + } + VStack { + Text("\(allSupersetExecerciseIndex+1)/\(workout.allSupersetExecercise?.count ?? 0)") + .font(Font.system(size: 55)) + .minimumScaleFactor(0.01) + .lineLimit(1) + .padding() + .bold() + .foregroundColor(.white) + .background( + Capsule() + .strokeBorder(Color.black, lineWidth: 0.8) + .background(Color(uiColor: UIColor(red: 148/255, + green: 0, + blue: 211/255, + alpha: 0.5))) + .clipped() + ) + .clipShape(Capsule()) + + Spacer() } - .onChange(of: currentExercise, perform: { newValue in - withAnimation { - print(newValue.id) - proxy.scrollTo(newValue.id, anchor: .top) - } - }) } } } diff --git a/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift b/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift index be80a60..d151db6 100644 --- a/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift +++ b/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift @@ -113,6 +113,12 @@ struct ExerciseListView: View { Text("\(superset.rounds) rounds") .foregroundColor(Color("appColor")) .bold() + + if let estimatedTime = superset.estimatedTime { + Text("@ " + estimatedTime.asString(style: .abbreviated)) + .foregroundColor(Color("appColor")) + .bold() + } } }) } diff --git a/Werkout_ios/Views/WorkoutDetail/InfoView.swift b/Werkout_ios/Views/WorkoutDetail/InfoView.swift index 53435b5..df74dad 100644 --- a/Werkout_ios/Views/WorkoutDetail/InfoView.swift +++ b/Werkout_ios/Views/WorkoutDetail/InfoView.swift @@ -18,12 +18,20 @@ struct InfoView: View { .frame(maxWidth: .infinity, alignment: .leading) .font(.title3) .padding() + if let desc = workout.description { Text(desc) .frame(maxWidth: .infinity, alignment: .leading) .font(.body) .padding([.leading, .trailing]) } + + if let estimatedTime = workout.estimatedTime { + Text(estimatedTime.asString(style: .abbreviated)) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.body) + .padding([.leading, .trailing]) + } } } } diff --git a/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift b/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift index 45e8cff..11cea4a 100644 --- a/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift +++ b/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift @@ -43,7 +43,6 @@ struct WorkoutDetailView: View { if phoneThotStyle != .off { GeometryReader { metrics in ZStack { - PlayerView(player: $avPlayer) .frame(width: metrics.size.width * 1, height: metrics.size.height * 1) .onAppear{