WIP
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RegisteredUser: Codable {
|
||||
struct RegisteredUser: Codable, Hashable {
|
||||
let id: Int
|
||||
let firstName, lastName, image: String?
|
||||
let nickName: String?
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: { })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user