add apple tv app
This commit is contained in:
49
iphone/LaunchScreen.storyboard
Normal file
49
iphone/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
|
||||
<rect key="frame" x="0.0" y="832" width="393" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon" translatesAutoresizingMaskIntoConstraints="NO" id="TO7-SX-zZe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" red="0.61568627450980395" green="0.54117647058823526" blue="1" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="trailing" secondItem="TO7-SX-zZe" secondAttribute="trailing" id="1uw-Tg-zq5"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
|
||||
<constraint firstItem="TO7-SX-zZe" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="5vV-DO-kiV"/>
|
||||
<constraint firstItem="TO7-SX-zZe" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="RIk-Do-dx8"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TO7-SX-zZe" secondAttribute="bottom" id="qmU-rS-Ali"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="icon" width="246" height="246"/>
|
||||
</resources>
|
||||
</document>
|
||||
15
iphone/Werkout-ios-Info.plist
Normal file
15
iphone/Werkout-ios-Info.plist
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
12
iphone/Werkout-watch-Watch-App-Info.plist
Normal file
12
iphone/Werkout-watch-Watch-App-Info.plist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array/>
|
||||
<key>WKBackgroundModes</key>
|
||||
<array>
|
||||
<string>workout-processing</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1043
iphone/Werkout_ios.xcodeproj/project.pbxproj
Normal file
1043
iphone/Werkout_ios.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
7
iphone/Werkout_ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
iphone/Werkout_ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
|
||||
BuildableName = "Werkout_ios.app"
|
||||
BlueprintName = "Werkout_ios"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
|
||||
BuildableName = "Werkout_ios.app"
|
||||
BlueprintName = "Werkout_ios"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
|
||||
BuildableName = "Werkout_ios.app"
|
||||
BlueprintName = "Werkout_ios"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
|
||||
BuildableName = "Werkout_watch Watch App.app"
|
||||
BlueprintName = "Werkout_watch Watch App"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
|
||||
BuildableName = "Werkout_ios.app"
|
||||
BlueprintName = "Werkout_ios"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
|
||||
BuildableName = "Werkout_watch Watch App.app"
|
||||
BlueprintName = "Werkout_watch Watch App"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
|
||||
BuildableName = "Werkout_watch Watch App.app"
|
||||
BlueprintName = "Werkout_watch Watch App"
|
||||
ReferencedContainer = "container:Werkout_ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
40
iphone/Werkout_ios/APIModels/AudioQueue.swift
Normal file
40
iphone/Werkout_ios/APIModels/AudioQueue.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// AudioQueue.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 8/12/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AudioType {
|
||||
case shortBeep
|
||||
case finishBeep
|
||||
case remoteURL(URL)
|
||||
}
|
||||
|
||||
struct AudioQueue: Codable, Identifiable, Equatable {
|
||||
var id = UUID()
|
||||
|
||||
let playAt: Int
|
||||
let audioURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case playAt = "play_at"
|
||||
case audioURL = "audio_url"
|
||||
}
|
||||
|
||||
var audioType: AudioType {
|
||||
if audioURL == "short_beep" {
|
||||
return .shortBeep
|
||||
} else if audioURL == "long_beep" {
|
||||
return .finishBeep
|
||||
} else {
|
||||
if let url = URL(string: BaseURLs.currentBaseURL + audioURL) {
|
||||
return .remoteURL(url)
|
||||
} else {
|
||||
return .shortBeep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
iphone/Werkout_ios/APIModels/CompletedWorkout.swift
Normal file
50
iphone/Werkout_ios/APIModels/CompletedWorkout.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// CompletedWorkout.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/21/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CompletedWorkout: Codable {
|
||||
let id: Int
|
||||
let workout: Workout
|
||||
let createdAt, updatedAt: String
|
||||
let difficulty, totalTime: Int?
|
||||
let workoutStartTime: String
|
||||
let notes: String?
|
||||
let totalCalories: Float?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, workout
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case difficulty
|
||||
case totalTime = "total_time"
|
||||
case workoutStartTime = "workout_start_time"
|
||||
case notes
|
||||
case totalCalories = "total_calories"
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletedWorkoutPOSTReturn: Codable {
|
||||
let id: Int
|
||||
let workout: Int
|
||||
let createdAt, updatedAt: String
|
||||
let difficulty, totalTime: Int?
|
||||
let workoutStartTime: String
|
||||
let notes: String?
|
||||
let totalCalories: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, workout
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case difficulty
|
||||
case totalTime = "total_time"
|
||||
case workoutStartTime = "workout_start_time"
|
||||
case notes
|
||||
case totalCalories = "total_calories"
|
||||
}
|
||||
}
|
||||
24
iphone/Werkout_ios/APIModels/Equipment.swift
Normal file
24
iphone/Werkout_ios/APIModels/Equipment.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Equipment.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Equipment: Codable, Identifiable, Equatable {
|
||||
let id: Int
|
||||
let equipment: Int?
|
||||
let name, createdAt, updatedAt: String
|
||||
let is_weight: Bool?
|
||||
let category: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, equipment
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case is_weight
|
||||
case category
|
||||
}
|
||||
}
|
||||
97
iphone/Werkout_ios/APIModels/Exercise.swift
Normal file
97
iphone/Werkout_ios/APIModels/Exercise.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Exercise.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SupersetExercise: Identifiable, Codable, Equatable, Hashable {
|
||||
var id: Int?
|
||||
|
||||
let workout: Int?
|
||||
let exercise: Exercise
|
||||
let weight: Int?
|
||||
let reps: Int?
|
||||
let duration: Int?
|
||||
let durationAudio: String?
|
||||
let weightAudio: String?
|
||||
let createdAt: String?
|
||||
let order, superset: Int?
|
||||
let uniqueID: String?
|
||||
let description: String?
|
||||
let audioQueues: [AudioQueue]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case workout, exercise, weight, reps, duration, order, superset, id, description
|
||||
case durationAudio = "duration_audio"
|
||||
case weightAudio = "weight_audio"
|
||||
case createdAt = "created_at"
|
||||
case uniqueID = "unique_id"
|
||||
case audioQueues = "audio_queues"
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
return hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
struct Exercise: Identifiable, Codable, Equatable, Hashable {
|
||||
static func == (lhs: Exercise, rhs: Exercise) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
return hasher.combine(id)
|
||||
}
|
||||
|
||||
let id: Int
|
||||
let equipment: [Equipment]
|
||||
let muscles: [Muscle]
|
||||
let audioURL, videoURL, createdAt, updatedAt: String
|
||||
let name, description: String
|
||||
let side, jointsUsed, movementPatterns: String?
|
||||
let isTwoDumbbells, isTrackableDistance, isAlternating, isWeight: Bool
|
||||
let isDistance, isDuration, isReps: Bool
|
||||
let equipmentRequired, muscleGroups: String?
|
||||
let synonyms: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, muscles, equipment
|
||||
case audioURL = "audio_url"
|
||||
case videoURL = "video_url"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case name, description, side
|
||||
case isTwoDumbbells = "is_two_dumbbells"
|
||||
case isTrackableDistance = "is_trackable_distance"
|
||||
case isAlternating = "is_alternating"
|
||||
case isWeight = "is_weight"
|
||||
case isDistance = "is_distance"
|
||||
case isDuration = "is_duration"
|
||||
case isReps = "is_reps"
|
||||
case jointsUsed = "joints_used"
|
||||
case movementPatterns = "movement_patterns"
|
||||
case equipmentRequired = "equipment_required"
|
||||
case muscleGroups = "muscle_groups"
|
||||
case synonyms
|
||||
}
|
||||
|
||||
var extName: String {
|
||||
if side != nil && side!.count > 0 {
|
||||
var returnString = name + " - " + side!
|
||||
returnString = returnString.replacingOccurrences(of: "_", with: " ")
|
||||
return returnString.capitalized
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
var spacedMuscleGroups: String {
|
||||
return muscleGroups?.replacingOccurrences(of: ",", with: ", ") ?? ""
|
||||
}
|
||||
|
||||
var spacedEquipmentRequired: String {
|
||||
return equipmentRequired?.replacingOccurrences(of: ",", with: ", ") ?? ""
|
||||
}
|
||||
}
|
||||
20
iphone/Werkout_ios/APIModels/Muscle.swift
Normal file
20
iphone/Werkout_ios/APIModels/Muscle.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Muscle.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Muscle: Codable, Identifiable, Equatable {
|
||||
let id: Int
|
||||
let muscle: Int?
|
||||
let name, createdAt, updatedAt: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, muscle
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
17
iphone/Werkout_ios/APIModels/NSFWVideo.swift
Normal file
17
iphone/Werkout_ios/APIModels/NSFWVideo.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// NSFWVideo.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/29/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NSFWVideo: Codable {
|
||||
let videoFile, genderValue: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case videoFile = "video_file"
|
||||
case genderValue = "gender_value"
|
||||
}
|
||||
}
|
||||
29
iphone/Werkout_ios/APIModels/PlannedWorkout.swift
Normal file
29
iphone/Werkout_ios/APIModels/PlannedWorkout.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// PlannedWorkout.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PlannedWorkout: Codable {
|
||||
let id: Int
|
||||
let createdAt, updatedAt, onDate: String
|
||||
let workout: Workout
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case onDate = "on_date"
|
||||
case workout
|
||||
}
|
||||
|
||||
var date: Date? {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "yyyy-MM-dd"
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
return df.date(from: self.onDate)
|
||||
}
|
||||
}
|
||||
31
iphone/Werkout_ios/APIModels/RegisteredUser.swift
Normal file
31
iphone/Werkout_ios/APIModels/RegisteredUser.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// RegisteredUser.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RegisteredUser: Codable, Hashable {
|
||||
let id: Int
|
||||
let firstName, lastName, image: String?
|
||||
let nickName: String?
|
||||
let token: String?
|
||||
let email: String?
|
||||
let hasNSFWToggle: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case firstName = "first_name"
|
||||
case lastName = "last_name"
|
||||
case image, token
|
||||
case email = "email_address"
|
||||
case nickName = "nick_name"
|
||||
case hasNSFWToggle = "has_nsfw_toggle"
|
||||
}
|
||||
|
||||
var NSFWValue: Bool {
|
||||
return hasNSFWToggle ?? false
|
||||
}
|
||||
}
|
||||
28
iphone/Werkout_ios/APIModels/Superset.swift
Normal file
28
iphone/Werkout_ios/APIModels/Superset.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Superset.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/17/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Superset: Codable, Identifiable, Hashable {
|
||||
let id: Int?
|
||||
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) {
|
||||
return hasher.combine(id)
|
||||
}
|
||||
}
|
||||
63
iphone/Werkout_ios/APIModels/Workout.swift
Normal file
63
iphone/Werkout_ios/APIModels/Workout.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Workout.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Workout: Codable, Identifiable, Equatable {
|
||||
static func == (lhs: Workout, rhs: Workout) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
let id: Int
|
||||
let name: String
|
||||
let description: String?
|
||||
let supersets: [Superset]?
|
||||
let registeredUser: RegisteredUser?
|
||||
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
|
||||
case registeredUser = "registered_user"
|
||||
case createdAt = "created_at"
|
||||
case estimatedTime = "estimated_time"
|
||||
case allSupersetExecercise = "all_superset_exercise"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "NA"
|
||||
self.description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
self.registeredUser = try container.decodeIfPresent(RegisteredUser.self, forKey: .registeredUser)
|
||||
self.id = try container.decode(Int.self, forKey: .id)
|
||||
self.supersets = try container.decodeIfPresent([Superset].self, forKey: .supersets)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "icon 1.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iphone/Werkout_ios/Assets.xcassets/AppIcon.appiconset/icon 1.png
Normal file
BIN
iphone/Werkout_ios/Assets.xcassets/AppIcon.appiconset/icon 1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 418 KiB |
BIN
iphone/Werkout_ios/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
BIN
iphone/Werkout_ios/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 418 KiB |
6
iphone/Werkout_ios/Assets.xcassets/Contents.json
Normal file
6
iphone/Werkout_ios/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemPurpleColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemPurpleColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
15
iphone/Werkout_ios/Assets.xcassets/icon.imageset/Contents.json
vendored
Normal file
15
iphone/Werkout_ios/Assets.xcassets/icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
BIN
iphone/Werkout_ios/Assets.xcassets/icon.imageset/icon.pdf
vendored
Normal file
BIN
iphone/Werkout_ios/Assets.xcassets/icon.imageset/icon.pdf
vendored
Normal file
Binary file not shown.
17
iphone/Werkout_ios/BaseURLs.swift
Normal file
17
iphone/Werkout_ios/BaseURLs.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// BaseURLs.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/10/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum BaseURLs: String {
|
||||
case local = "http://127.0.0.1:8000"
|
||||
case dev = "https://dev.werkout.fitness"
|
||||
|
||||
static var currentBaseURL: String {
|
||||
return BaseURLs.dev.rawValue
|
||||
}
|
||||
}
|
||||
386
iphone/Werkout_ios/BridgeModule.swift
Normal file
386
iphone/Werkout_ios/BridgeModule.swift
Normal file
@@ -0,0 +1,386 @@
|
||||
//
|
||||
// TimerModule.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
import AVFoundation
|
||||
import HealthKit
|
||||
|
||||
enum WatchActions: Codable {
|
||||
case nextExercise
|
||||
case restartExercise
|
||||
case previousExercise
|
||||
case stopWorkout
|
||||
case pauseWorkout
|
||||
case workoutComplete(Data)
|
||||
}
|
||||
|
||||
enum PhoneToWatchActions: Codable {
|
||||
case inExercise(WatchPackageModel)
|
||||
case reset
|
||||
case endWorkout
|
||||
}
|
||||
|
||||
class BridgeModule: NSObject, ObservableObject {
|
||||
private let kMessageKey = "message"
|
||||
|
||||
static let shared = BridgeModule()
|
||||
@Published var isShowingOnExternalDisplay = false
|
||||
|
||||
@Published var isInWorkout = false
|
||||
var completedWorkout: (() -> Void)?
|
||||
@Published var currentWorkoutRunTimeInSeconds: Int = -1
|
||||
private var currentWorkoutRunTimer: Timer?
|
||||
|
||||
public private(set) var workoutStartDate: Date?
|
||||
|
||||
private var currentExerciseTimer: Timer?
|
||||
|
||||
@Published public private(set) var currentExerciseInfo = CurrentWorkoutInfo()
|
||||
@Published var previewWorkout: Workout?
|
||||
|
||||
@Published var currentExerciseTimeLeft: Int = 0
|
||||
|
||||
private var isWatchConnected = false
|
||||
// workoutEndDate fills out WatchPackageModel.workoutEndDate which
|
||||
// tells the watch app to stop the workout
|
||||
public private(set) var workoutEndDate: Date?
|
||||
@Published public private(set) var healthKitUUID: UUID?
|
||||
@Published var isPaused = false
|
||||
|
||||
var audioPlayer: AVAudioPlayer?
|
||||
var avPlayer: AVPlayer?
|
||||
|
||||
func start(workout: Workout) {
|
||||
currentExerciseInfo.complete = {
|
||||
self.completeWorkout()
|
||||
}
|
||||
|
||||
currentExerciseInfo.start(workout: workout)
|
||||
currentWorkoutRunTimeInSeconds = 0
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
isPaused = false
|
||||
|
||||
if let superetExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
startWorkoutTimer()
|
||||
workoutStartDate = Date()
|
||||
isInWorkout = true
|
||||
|
||||
if WCSession.isSupported() {
|
||||
WCSession.default.delegate = self
|
||||
WCSession.default.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func goToExerciseAt(section: Int, row: Int) {
|
||||
if let superetExercise = currentExerciseInfo.goToWorkoutAt(supersetIndex: section,
|
||||
exerciseIndex: row) {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
}
|
||||
}
|
||||
|
||||
var nextExerciseObject: SupersetExercise? {
|
||||
currentExerciseInfo.goToNextExercise
|
||||
}
|
||||
|
||||
func resetCurrentWorkout() {
|
||||
DispatchQueue.main.async {
|
||||
self.currentWorkoutRunTimeInSeconds = 0
|
||||
self.currentWorkoutRunTimer?.invalidate()
|
||||
self.currentWorkoutRunTimer = nil
|
||||
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
self.currentWorkoutRunTimeInSeconds = -1
|
||||
self.currentExerciseInfo.reset()
|
||||
|
||||
self.isInWorkout = false
|
||||
self.workoutStartDate = nil
|
||||
self.workoutEndDate = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func startWorkoutTimer() {
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
currentWorkoutRunTimer = Timer.scheduledTimer(timeInterval: 1,
|
||||
target: self,
|
||||
selector: #selector(addOneToWorkoutRunTime),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
currentWorkoutRunTimer?.fire()
|
||||
}
|
||||
|
||||
private func startExerciseTimerWith(duration: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.currentExerciseTimeLeft = duration
|
||||
self.currentExerciseTimer = Timer.scheduledTimer(timeInterval: 1,
|
||||
target: self,
|
||||
selector: #selector(self.updateCurrentExerciseTimer),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
self.currentExerciseTimer?.fire()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateCurrentExerciseTimer() {
|
||||
if currentExerciseTimeLeft > 1 {
|
||||
currentExerciseTimeLeft -= 1
|
||||
|
||||
if let currentExercise = currentExerciseInfo.allSupersetExecercise, let audioQueues = currentExercise.audioQueues {
|
||||
if let audioQueue = audioQueues.first(where: {
|
||||
$0.playAt == currentExerciseTimeLeft
|
||||
}) {
|
||||
switch audioQueue.audioType {
|
||||
|
||||
case .shortBeep:
|
||||
playBeep()
|
||||
case .finishBeep:
|
||||
playFinished()
|
||||
case .remoteURL(let url):
|
||||
playRemoteAudio(fromURL: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
sendCurrentExerciseToWatch()
|
||||
} else {
|
||||
nextExercise()
|
||||
}
|
||||
}
|
||||
|
||||
func pauseWorkout() {
|
||||
if let _ = currentExerciseTimer {
|
||||
currentExerciseTimer?.invalidate()
|
||||
currentExerciseTimer = nil
|
||||
isPaused = true
|
||||
} else {
|
||||
isPaused = false
|
||||
startExerciseTimerWith(duration: currentExerciseTimeLeft)
|
||||
}
|
||||
}
|
||||
|
||||
func nextExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.goToNextExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func previousExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.previousExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func restartExercise() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: currentExercise)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func addOneToWorkoutRunTime() {
|
||||
currentWorkoutRunTimeInSeconds += 1
|
||||
}
|
||||
|
||||
func updateCurrent(exercise: SupersetExercise) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
if let duration = exercise.duration, duration > 0 {
|
||||
self.startExerciseTimerWith(duration: duration)
|
||||
}
|
||||
self.sendCurrentExerciseToWatch()
|
||||
}
|
||||
}
|
||||
|
||||
func completeWorkout() {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.isInWorkout = false
|
||||
|
||||
workoutEndDate = Date()
|
||||
if let completedWorkout = completedWorkout {
|
||||
completedWorkout()
|
||||
self.completedWorkout = nil
|
||||
}
|
||||
}
|
||||
|
||||
func playRemoteAudio(fromURL url: URL) {
|
||||
#if os(iOS)
|
||||
let playerItem = AVPlayerItem(url: url)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
avPlayer = AVPlayer(playerItem: playerItem)
|
||||
avPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func playBeep() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "short_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func playFinished() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "long_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension BridgeModule: WCSessionDelegate {
|
||||
func sendResetToWatch() {
|
||||
let watchModel = PhoneToWatchActions.reset
|
||||
let data = try! JSONEncoder().encode(watchModel)
|
||||
send(data)
|
||||
}
|
||||
|
||||
func sendWorkoutCompleteToWatch() {
|
||||
if WCSession.default.isReachable {
|
||||
let model = PhoneToWatchActions.endWorkout
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
send(data)
|
||||
}
|
||||
}
|
||||
|
||||
func sendCurrentExerciseToWatch() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let duration = currentExercise.duration ,
|
||||
duration > 0 {
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
send(data)
|
||||
} else {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let reps = currentExercise.reps,
|
||||
reps > 0 {
|
||||
|
||||
// if not a timer we need to set the watch display with number of reps
|
||||
// if timer it will set when timer updates
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
self.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
|
||||
if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) {
|
||||
switch model {
|
||||
case .nextExercise:
|
||||
nextExercise()
|
||||
playFinished()
|
||||
case .workoutComplete(let data):
|
||||
DispatchQueue.main.async {
|
||||
let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data)
|
||||
self.healthKitUUID = model.healthKitUUID
|
||||
}
|
||||
case .restartExercise:
|
||||
restartExercise()
|
||||
case .previousExercise:
|
||||
previousExercise()
|
||||
case .stopWorkout:
|
||||
completeWorkout()
|
||||
case .pauseWorkout:
|
||||
pauseWorkout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func session(_ session: WCSession,
|
||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||
error: Error?) {
|
||||
switch activationState {
|
||||
case .notActivated:
|
||||
print("notActivated")
|
||||
case .inactive:
|
||||
print("inactive")
|
||||
case .activated:
|
||||
print("activated")
|
||||
#if os(iOS)
|
||||
let workoutConfiguration = HKWorkoutConfiguration()
|
||||
workoutConfiguration.activityType = .functionalStrengthTraining
|
||||
workoutConfiguration.locationType = .indoor
|
||||
if WCSession.isSupported(), WCSession.default.activationState == .activated, WCSession.default.isWatchAppInstalled {
|
||||
HKHealthStore().startWatchApp(with: workoutConfiguration, completion: { (success, error) in
|
||||
print(error.debugDescription)
|
||||
})
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {
|
||||
|
||||
}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
#endif
|
||||
func send(_ data: Data) {
|
||||
guard WCSession.default.activationState == .activated else {
|
||||
return
|
||||
}
|
||||
#if os(iOS)
|
||||
guard WCSession.default.isWatchAppInstalled else {
|
||||
return
|
||||
}
|
||||
#else
|
||||
guard WCSession.default.isCompanionAppInstalled else {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
WCSession.default.sendMessageData(data, replyHandler: nil) { error in
|
||||
print("Cannot send message: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
157
iphone/Werkout_ios/CurrentWorkoutInfo.swift
Normal file
157
iphone/Werkout_ios/CurrentWorkoutInfo.swift
Normal file
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// CurrentWorkoutInfo.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/17/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CurrentWorkoutInfo {
|
||||
var supersetIndex: Int = 0
|
||||
var exerciseIndex: Int = -1
|
||||
var workout: Workout?
|
||||
var complete: (() -> Void)?
|
||||
|
||||
var currentRound = 1
|
||||
var allSupersetExecerciseIndex = 0
|
||||
|
||||
var superset: [Superset] {
|
||||
return workout?.supersets?.sorted(by: { $0.order < $1.order }) ?? [Superset]()
|
||||
}
|
||||
|
||||
var numberOfRoundsInCurrentSuperSet: Int {
|
||||
guard let workout = workout else { return -1 }
|
||||
guard let supersets = workout.supersets else { return -1 }
|
||||
|
||||
if supersetIndex >= supersets.count {
|
||||
return -1
|
||||
}
|
||||
let currentSuperSet = supersets[supersetIndex]
|
||||
|
||||
if exerciseIndex >= currentSuperSet.exercises.count {
|
||||
return -1
|
||||
}
|
||||
|
||||
return currentSuperSet.rounds
|
||||
}
|
||||
|
||||
var currentExercise: SupersetExercise? {
|
||||
guard let supersets = workout?.supersets else { return nil }
|
||||
|
||||
if supersetIndex >= supersets.count { return nil }
|
||||
let superset = supersets[supersetIndex]
|
||||
|
||||
// will be -1 for a moment while going to previous workout / superset
|
||||
if exerciseIndex < 0 { return nil }
|
||||
if exerciseIndex > superset.exercises.count { return nil }
|
||||
let exercise = superset.exercises[exerciseIndex]
|
||||
return exercise
|
||||
}
|
||||
|
||||
var allSupersetExecercise: SupersetExercise? {
|
||||
let obj = workout?.allSupersetExecercise?[allSupersetExecerciseIndex]
|
||||
return obj
|
||||
}
|
||||
|
||||
// this is setting nothing so we can half ass it
|
||||
var nextExerciseInfo: SupersetExercise? {
|
||||
guard let workout = workout else { return nil }
|
||||
|
||||
let _exerciseIndex = allSupersetExecerciseIndex + 1
|
||||
if _exerciseIndex >= workout.allSupersetExecercise?.count ?? -1 {
|
||||
return nil
|
||||
}
|
||||
return workout.allSupersetExecercise?[_exerciseIndex]
|
||||
}
|
||||
|
||||
// this needs to set stuff for iphone
|
||||
var goToNextExercise: SupersetExercise? {
|
||||
guard let workout = workout else { return nil }
|
||||
guard let supersets = workout.supersets else { return nil }
|
||||
|
||||
exerciseIndex += 1
|
||||
let currentSuperSet = supersets[supersetIndex]
|
||||
|
||||
if exerciseIndex >= currentSuperSet.exercises.count {
|
||||
currentRound += 1
|
||||
|
||||
if currentRound > currentSuperSet.rounds {
|
||||
supersetIndex += 1
|
||||
currentRound = 1
|
||||
|
||||
if supersetIndex >= supersets.count {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
exerciseIndex = 0
|
||||
}
|
||||
|
||||
let superset = supersets[supersetIndex]
|
||||
let exercise = superset.exercises[exerciseIndex]
|
||||
allSupersetExecerciseIndex += 1
|
||||
return exercise
|
||||
}
|
||||
|
||||
var previousExercise: SupersetExercise? {
|
||||
guard let workout = workout else { return nil }
|
||||
guard let supersets = workout.supersets else { return nil }
|
||||
|
||||
exerciseIndex -= 1
|
||||
if exerciseIndex < 0 {
|
||||
if currentRound > 1 {
|
||||
currentRound -= 1
|
||||
let superset = supersets[supersetIndex]
|
||||
exerciseIndex = superset.exercises.count-1
|
||||
} else {
|
||||
if supersetIndex > 0 {
|
||||
supersetIndex -= 1
|
||||
let superset = supersets[supersetIndex]
|
||||
currentRound = superset.rounds
|
||||
exerciseIndex = superset.exercises.count-1
|
||||
} else {
|
||||
exerciseIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let superset = supersets[supersetIndex]
|
||||
let exercise = superset.exercises[exerciseIndex]
|
||||
|
||||
allSupersetExecerciseIndex -= 1
|
||||
if allSupersetExecerciseIndex < 0 {
|
||||
allSupersetExecerciseIndex = 0
|
||||
}
|
||||
|
||||
return exercise
|
||||
}
|
||||
|
||||
func start(workout: Workout) {
|
||||
reset()
|
||||
self.workout = workout
|
||||
}
|
||||
|
||||
func reset() {
|
||||
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
|
||||
}
|
||||
}
|
||||
132
iphone/Werkout_ios/DataStore.swift
Normal file
132
iphone/Werkout_ios/DataStore.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// DataStore.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/19/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class DataStore: ObservableObject {
|
||||
enum DataStoreStatus {
|
||||
case loading
|
||||
case idle
|
||||
}
|
||||
|
||||
static let shared = DataStore()
|
||||
|
||||
public private(set) var allWorkouts: [Workout]?
|
||||
public private(set) var allMuscles: [Muscle]?
|
||||
public private(set) var allEquipment: [Equipment]?
|
||||
public private(set) var allExercise: [Exercise]?
|
||||
public private(set) var allNSFWVideos: [NSFWVideo]?
|
||||
|
||||
@Published public private(set) var status = DataStoreStatus.idle
|
||||
|
||||
private let fetchAllDataQueue = DispatchGroup()
|
||||
|
||||
public func randomVideoFor(gender: String) -> String? {
|
||||
return allNSFWVideos?.filter({
|
||||
$0.genderValue.lowercased() == gender.lowercased()
|
||||
}).randomElement()?.videoFile ?? nil
|
||||
}
|
||||
|
||||
public var nsfwGenderOptions: [String]? {
|
||||
let values = self.allNSFWVideos?.map({
|
||||
$0.genderValue
|
||||
})
|
||||
if let values = values {
|
||||
return Array(Set(values))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
fetchAllDataQueue.enter()
|
||||
fetchAllDataQueue.enter()
|
||||
fetchAllDataQueue.enter()
|
||||
fetchAllDataQueue.enter()
|
||||
|
||||
fetchAllDataQueue.notify(queue: .main) {
|
||||
self.status = .idle
|
||||
completion()
|
||||
}
|
||||
|
||||
AllWorkoutFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
self.allWorkouts = model
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
self.fetchAllDataQueue.leave()
|
||||
})
|
||||
|
||||
AllMusclesFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
self.allMuscles = model.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
self.fetchAllDataQueue.leave()
|
||||
})
|
||||
|
||||
AllEquipmentFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
self.allEquipment = model.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
self.fetchAllDataQueue.leave()
|
||||
})
|
||||
|
||||
AllExerciseFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
self.allExercise = model.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
self.fetchAllDataQueue.leave()
|
||||
})
|
||||
|
||||
AllNSFWVideosFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
self.allNSFWVideos = model
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
self.fetchAllDataQueue.leave()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func setupFakeData() {
|
||||
allWorkouts = PreviewData.allWorkouts()
|
||||
allMuscles = PreviewData.parseMuscle()
|
||||
allEquipment = PreviewData.parseEquipment()
|
||||
allExercise = PreviewData.parseExercises()
|
||||
}
|
||||
}
|
||||
141
iphone/Werkout_ios/Extensions.swift
Normal file
141
iphone/Werkout_ios/Extensions.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/20/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension Dictionary {
|
||||
func percentEncoded() -> Data? {
|
||||
map { key, value in
|
||||
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
|
||||
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
|
||||
return escapedKey + "=" + escapedValue
|
||||
}
|
||||
.joined(separator: "&")
|
||||
.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension CharacterSet {
|
||||
static let urlQueryValueAllowed: CharacterSet = {
|
||||
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
|
||||
let subDelimitersToEncode = "!$&'()*+,;="
|
||||
|
||||
var allowed: CharacterSet = .urlQueryAllowed
|
||||
allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
|
||||
return allowed
|
||||
}()
|
||||
}
|
||||
|
||||
extension Date {
|
||||
var timeFormatForUpload: String {
|
||||
let isoFormatter = DateFormatter()
|
||||
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
|
||||
return isoFormatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var dateFromServerDate: Date? {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
return df.date(from: self)
|
||||
}
|
||||
|
||||
var plannedDate: Date? {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "yyyy-MM-dd"
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
return df.date(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents {
|
||||
return calendar.dateComponents(Set(components), from: self)
|
||||
}
|
||||
|
||||
func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int {
|
||||
return calendar.component(component, from: self)
|
||||
}
|
||||
|
||||
var formatForPlannedWorkout: String {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "yyyy-MM-dd"
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
return df.string(from: self)
|
||||
}
|
||||
|
||||
var weekDay: String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "EEE"
|
||||
let weekDay = dateFormatter.string(from: self)
|
||||
return weekDay
|
||||
}
|
||||
|
||||
var monthString: String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "MMM"
|
||||
let weekDay = dateFormatter.string(from: self)
|
||||
return weekDay
|
||||
}
|
||||
|
||||
var dateString: String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "d"
|
||||
let weekDay = dateFormatter.string(from: self)
|
||||
return weekDay
|
||||
}
|
||||
}
|
||||
|
||||
extension Bundle {
|
||||
public var icon: UIImage? {
|
||||
if let icons = infoDictionary?["CFBundleIcons"] as? [String: Any],
|
||||
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
|
||||
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
|
||||
let lastIcon = iconFiles.last {
|
||||
return UIImage(named: lastIcon)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Double {
|
||||
/*
|
||||
10000.asString(style: .positional) // 2:46:40
|
||||
10000.asString(style: .abbreviated) // 2h 46m 40s
|
||||
10000.asString(style: .short) // 2 hr, 46 min, 40 sec
|
||||
10000.asString(style: .full) // 2 hours, 46 minutes, 40 seconds
|
||||
10000.asString(style: .spellOut) // two hours, forty-six minutes, forty seconds
|
||||
10000.asString(style: .brief) // 2hr 46min 40sec
|
||||
*/
|
||||
func asString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second, .nanosecond]
|
||||
formatter.unitsStyle = style
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
clipShape( RoundedCorner(radius: radius, corners: corners) )
|
||||
}
|
||||
}
|
||||
|
||||
struct RoundedCorner: Shape {
|
||||
|
||||
var radius: CGFloat = .infinity
|
||||
var corners: UIRectCorner = .allCorners
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
128
iphone/Werkout_ios/HealthKitHelper.swift
Normal file
128
iphone/Werkout_ios/HealthKitHelper.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// HealthKitHelper.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 8/13/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
struct HealthKitWorkoutData {
|
||||
var caloriesBurned: Double?
|
||||
var minHeartRate: Double?
|
||||
var maxHeartRate: Double?
|
||||
var avgHeartRate: Double?
|
||||
}
|
||||
|
||||
class HealthKitHelper {
|
||||
// this is dirty and i dont care
|
||||
var returnCount = 0
|
||||
let healthStore = HKHealthStore()
|
||||
|
||||
var healthKitWorkoutData = HealthKitWorkoutData(
|
||||
caloriesBurned: nil,
|
||||
minHeartRate: nil,
|
||||
maxHeartRate: nil,
|
||||
avgHeartRate: nil)
|
||||
|
||||
var completion: ((HealthKitWorkoutData) -> Void)?
|
||||
|
||||
func getDetails(forHealthKitUUID uuid: UUID, completion: @escaping ((HealthKitWorkoutData) -> Void)) {
|
||||
self.completion = completion
|
||||
self.returnCount = 0
|
||||
|
||||
print("get details \(uuid.uuidString)")
|
||||
|
||||
let predicate = HKQuery.predicateForObject(with: uuid)
|
||||
let query = HKSampleQuery(sampleType: HKWorkoutType.workoutType(),
|
||||
predicate: predicate,
|
||||
limit: 0,
|
||||
sortDescriptors: nil)
|
||||
{ (sampleQuery, results, error ) -> Void in
|
||||
|
||||
if let queryError = error {
|
||||
print( "There was an error while reading the samples: \(queryError.localizedDescription)")
|
||||
} else {
|
||||
for samples: HKSample in results! {
|
||||
let workout: HKWorkout = (samples as! HKWorkout)
|
||||
self.getTotalBurned(forWorkout: workout)
|
||||
self.getHeartRateStuff(forWorkout: workout)
|
||||
print("got workout")
|
||||
}
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
func getHeartRateStuff(forWorkout workout: HKWorkout) {
|
||||
print("get heart")
|
||||
let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate)
|
||||
let heartPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let heartQuery = HKStatisticsQuery(quantityType: heartType!,
|
||||
quantitySamplePredicate: heartPredicate,
|
||||
options: [.discreteAverage, .discreteMin, .discreteMax],
|
||||
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
|
||||
if let result = result,
|
||||
let minValue = result.minimumQuantity(),
|
||||
let maxValue = result.maximumQuantity(),
|
||||
let avgValue = result.averageQuantity() {
|
||||
|
||||
let _minHeartRate = minValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
)
|
||||
|
||||
let _maxHeartRate = maxValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
)
|
||||
|
||||
let _avgHeartRate = avgValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
)
|
||||
self.healthKitWorkoutData.avgHeartRate = _avgHeartRate
|
||||
self.healthKitWorkoutData.minHeartRate = _minHeartRate
|
||||
self.healthKitWorkoutData.maxHeartRate = _maxHeartRate
|
||||
print("got heart")
|
||||
DispatchQueue.main.async {
|
||||
self.shitReturned()
|
||||
}
|
||||
}
|
||||
})
|
||||
healthStore.execute(heartQuery)
|
||||
}
|
||||
|
||||
func getTotalBurned(forWorkout workout: HKWorkout) {
|
||||
print("get total burned")
|
||||
let calType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)
|
||||
let calPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let calQuery = HKStatisticsQuery(quantityType: calType!,
|
||||
quantitySamplePredicate: calPredicate,
|
||||
options: [.cumulativeSum],
|
||||
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
|
||||
if let result = result {
|
||||
self.healthKitWorkoutData.caloriesBurned = result.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? -1
|
||||
print("got total burned")
|
||||
DispatchQueue.main.async {
|
||||
self.shitReturned()
|
||||
}
|
||||
}
|
||||
})
|
||||
healthStore.execute(calQuery)
|
||||
}
|
||||
|
||||
func shitReturned() {
|
||||
DispatchQueue.main.async {
|
||||
self.returnCount += 1
|
||||
print("\(self.returnCount)")
|
||||
if self.returnCount == 2 {
|
||||
self.completion!(self.healthKitWorkoutData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
iphone/Werkout_ios/JSON/AllMuscles.json
Normal file
230
iphone/Werkout_ios/JSON/AllMuscles.json
Normal file
@@ -0,0 +1,230 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": "2023-06-13T01:58:13.212264Z",
|
||||
"updated_at": "2023-06-13T01:58:13.212311Z",
|
||||
"name": "deltoids"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"created_at": "2023-06-13T01:58:13.214487Z",
|
||||
"updated_at": "2023-06-13T01:58:13.214520Z",
|
||||
"name": "core"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"created_at": "2023-06-13T01:58:13.215255Z",
|
||||
"updated_at": "2023-06-13T01:58:13.215277Z",
|
||||
"name": "obliques"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"created_at": "2023-06-13T01:58:13.216495Z",
|
||||
"updated_at": "2023-06-13T01:58:13.216522Z",
|
||||
"name": "glutes"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"created_at": "2023-06-13T01:58:13.217347Z",
|
||||
"updated_at": "2023-06-13T01:58:13.217371Z",
|
||||
"name": "quads"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"created_at": "2023-06-13T01:58:13.218407Z",
|
||||
"updated_at": "2023-06-13T01:58:13.218434Z",
|
||||
"name": "hamstrings"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"created_at": "2023-06-13T01:58:13.219494Z",
|
||||
"updated_at": "2023-06-13T01:58:13.219521Z",
|
||||
"name": "hip abductors"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"created_at": "2023-06-13T01:58:13.220702Z",
|
||||
"updated_at": "2023-06-13T01:58:13.220737Z",
|
||||
"name": "triceps"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"created_at": "2023-06-13T01:58:13.221543Z",
|
||||
"updated_at": "2023-06-13T01:58:13.221564Z",
|
||||
"name": "upper back"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"created_at": "2023-06-13T01:58:13.222148Z",
|
||||
"updated_at": "2023-06-13T01:58:13.222166Z",
|
||||
"name": "lats"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"created_at": "2023-06-13T01:58:13.222710Z",
|
||||
"updated_at": "2023-06-13T01:58:13.222729Z",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"created_at": "2023-06-13T01:58:13.223248Z",
|
||||
"updated_at": "2023-06-13T01:58:13.223267Z",
|
||||
"name": "Abs"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"created_at": "2023-06-13T01:58:13.223787Z",
|
||||
"updated_at": "2023-06-13T01:58:13.223805Z",
|
||||
"name": "middle back"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"created_at": "2023-06-13T01:58:13.224313Z",
|
||||
"updated_at": "2023-06-13T01:58:13.224330Z",
|
||||
"name": "biceps"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"created_at": "2023-06-13T01:58:13.224843Z",
|
||||
"updated_at": "2023-06-13T01:58:13.224860Z",
|
||||
"name": "rotator cuff"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"created_at": "2023-06-13T01:58:13.225377Z",
|
||||
"updated_at": "2023-06-13T01:58:13.225394Z",
|
||||
"name": "hip flexor"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"created_at": "2023-06-13T01:58:13.226340Z",
|
||||
"updated_at": "2023-06-13T01:58:13.226370Z",
|
||||
"name": "Quads"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"created_at": "2023-06-13T01:58:13.227071Z",
|
||||
"updated_at": "2023-06-13T01:58:13.227094Z",
|
||||
"name": "Glutes"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"created_at": "2023-06-13T01:58:13.227668Z",
|
||||
"updated_at": "2023-06-13T01:58:13.227687Z",
|
||||
"name": "lower back"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"created_at": "2023-06-13T01:58:13.228176Z",
|
||||
"updated_at": "2023-06-13T01:58:13.228193Z",
|
||||
"name": "chest"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"created_at": "2023-06-13T01:58:13.228647Z",
|
||||
"updated_at": "2023-06-13T01:58:13.228663Z",
|
||||
"name": "Shoulders"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"created_at": "2023-06-13T01:58:13.229110Z",
|
||||
"updated_at": "2023-06-13T01:58:13.229126Z",
|
||||
"name": "calves"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"created_at": "2023-06-13T01:58:13.229566Z",
|
||||
"updated_at": "2023-06-13T01:58:13.229581Z",
|
||||
"name": "Obliques"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"created_at": "2023-06-13T01:58:13.230014Z",
|
||||
"updated_at": "2023-06-13T01:58:13.230031Z",
|
||||
"name": "Hip Flexor"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"created_at": "2023-06-13T01:58:13.230457Z",
|
||||
"updated_at": "2023-06-13T01:58:13.230472Z",
|
||||
"name": "Middle Back"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"created_at": "2023-06-13T01:58:13.230905Z",
|
||||
"updated_at": "2023-06-13T01:58:13.230919Z",
|
||||
"name": "Lats"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"created_at": "2023-06-13T01:58:13.231350Z",
|
||||
"updated_at": "2023-06-13T01:58:13.231364Z",
|
||||
"name": "hip adductors"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"created_at": "2023-06-13T01:58:13.231849Z",
|
||||
"updated_at": "2023-06-13T01:58:13.231864Z",
|
||||
"name": "traps"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"created_at": "2023-06-13T01:58:13.232295Z",
|
||||
"updated_at": "2023-06-13T01:58:13.232310Z",
|
||||
"name": "forearms"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"created_at": "2023-06-13T01:58:13.232733Z",
|
||||
"updated_at": "2023-06-13T01:58:13.232748Z",
|
||||
"name": "abs"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"created_at": "2023-06-13T01:58:13.233175Z",
|
||||
"updated_at": "2023-06-13T01:58:13.233192Z",
|
||||
"name": "Biceps"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"created_at": "2023-06-13T01:58:13.233616Z",
|
||||
"updated_at": "2023-06-13T01:58:13.233631Z",
|
||||
"name": "Triceps"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"created_at": "2023-06-13T01:58:13.234054Z",
|
||||
"updated_at": "2023-06-13T01:58:13.234068Z",
|
||||
"name": "Core"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"created_at": "2023-06-13T01:58:13.234489Z",
|
||||
"updated_at": "2023-06-13T01:58:13.234503Z",
|
||||
"name": "intercostals"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"created_at": "2023-06-13T01:58:13.234927Z",
|
||||
"updated_at": "2023-06-13T01:58:13.234941Z",
|
||||
"name": "Chest"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"created_at": "2023-06-13T01:58:13.235368Z",
|
||||
"updated_at": "2023-06-13T01:58:13.235383Z",
|
||||
"name": "feet"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"created_at": "2023-06-13T01:58:13.236126Z",
|
||||
"updated_at": "2023-06-13T01:58:13.236153Z",
|
||||
"name": "arms"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"created_at": "2023-06-13T01:58:13.236718Z",
|
||||
"updated_at": "2023-06-13T01:58:13.236734Z",
|
||||
"name": "it band"
|
||||
}
|
||||
]
|
||||
436
iphone/Werkout_ios/JSON/AllWorkouts.json
Normal file
436
iphone/Werkout_ios/JSON/AllWorkouts.json
Normal file
@@ -0,0 +1,436 @@
|
||||
[
|
||||
{
|
||||
"id": 3,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"quads",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower"
|
||||
],
|
||||
"exercise_count": 14,
|
||||
"created_at": "2023-07-03T17:00:01.350058Z",
|
||||
"updated_at": "2023-07-04T16:51:07.231366Z",
|
||||
"name": "Rowwwwwwer",
|
||||
"description": "row until you pass out"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"quads",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower"
|
||||
],
|
||||
"exercise_count": 30,
|
||||
"created_at": "2023-07-04T17:36:56.902038Z",
|
||||
"updated_at": "2023-07-04T18:02:18.798549Z",
|
||||
"name": "Row 2",
|
||||
"description": "Let’s pass out"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"calves",
|
||||
"quads",
|
||||
"core",
|
||||
"hip flexor",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower",
|
||||
"Yoga Mat"
|
||||
],
|
||||
"exercise_count": 60,
|
||||
"created_at": "2023-07-05T16:16:57.374041Z",
|
||||
"updated_at": "2023-07-05T16:16:57.378248Z",
|
||||
"name": "Cardio Mix",
|
||||
"description": "Mix of cardio stuff"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"calves",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"core",
|
||||
"quads",
|
||||
"hip flexor",
|
||||
"middle back",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower",
|
||||
"Yoga Mat"
|
||||
],
|
||||
"exercise_count": 56,
|
||||
"created_at": "2023-07-05T17:15:16.596202Z",
|
||||
"updated_at": "2023-07-05T17:15:16.597621Z",
|
||||
"name": "Cardio mix #2",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"glutes",
|
||||
"quads",
|
||||
"core",
|
||||
"triceps",
|
||||
"deltoids",
|
||||
"hip flexor",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Bike",
|
||||
"Battle Ropes"
|
||||
],
|
||||
"exercise_count": 24,
|
||||
"created_at": "2023-07-06T14:47:57.669190Z",
|
||||
"updated_at": "2023-07-06T14:47:57.673138Z",
|
||||
"name": "HiiT #1",
|
||||
"description": "Upper body tabata"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"glutes",
|
||||
"quads",
|
||||
"feet"
|
||||
],
|
||||
"equipment": [],
|
||||
"exercise_count": 24,
|
||||
"created_at": "2023-07-06T14:49:24.336730Z",
|
||||
"updated_at": "2023-07-06T14:49:24.338220Z",
|
||||
"name": "HiiT #2",
|
||||
"description": "Lower body tabata"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"rotator cuff",
|
||||
"chest",
|
||||
"core",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"triceps",
|
||||
"quads",
|
||||
"abs",
|
||||
"middle back",
|
||||
"forearms",
|
||||
"lats",
|
||||
"hip abductors",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Pull-Up Bar",
|
||||
"Kettlebell"
|
||||
],
|
||||
"exercise_count": 20,
|
||||
"created_at": "2023-07-06T14:51:00.277200Z",
|
||||
"updated_at": "2023-07-06T14:51:00.278874Z",
|
||||
"name": "HiiT #3",
|
||||
"description": "Total body"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"chest",
|
||||
"biceps",
|
||||
"forearms",
|
||||
"triceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Dumbbell",
|
||||
"Barbell",
|
||||
"Plates",
|
||||
"Bench"
|
||||
],
|
||||
"exercise_count": 24,
|
||||
"created_at": "2023-07-06T14:56:03.908583Z",
|
||||
"updated_at": "2023-07-06T14:56:03.910350Z",
|
||||
"name": "Arms and HiiT",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"hamstrings",
|
||||
"traps",
|
||||
"core",
|
||||
"quads",
|
||||
"glutes",
|
||||
"middle back",
|
||||
"forearms",
|
||||
"lats",
|
||||
"lower back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Dumbbell",
|
||||
"Barbell",
|
||||
"Plates"
|
||||
],
|
||||
"exercise_count": 21,
|
||||
"created_at": "2023-07-06T15:00:42.677119Z",
|
||||
"updated_at": "2023-07-06T15:00:42.678641Z",
|
||||
"name": "Upper Pull",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"core",
|
||||
"middle back",
|
||||
"forearms",
|
||||
"lats",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Barbell",
|
||||
"Dumbbell",
|
||||
"Plates",
|
||||
"Chest Supported Row Machine",
|
||||
"Bench"
|
||||
],
|
||||
"exercise_count": 32,
|
||||
"created_at": "2023-07-06T15:06:57.002749Z",
|
||||
"updated_at": "2023-07-06T15:06:57.004148Z",
|
||||
"name": "Upper Pull",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"deltoids",
|
||||
"chest",
|
||||
"core",
|
||||
"triceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Dumbbell",
|
||||
"Barbell",
|
||||
"Box",
|
||||
"Bench"
|
||||
],
|
||||
"exercise_count": 29,
|
||||
"created_at": "2023-07-06T15:11:52.085325Z",
|
||||
"updated_at": "2023-07-06T15:11:52.086738Z",
|
||||
"name": "Upper push",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"rotator cuff",
|
||||
"chest",
|
||||
"core",
|
||||
"traps",
|
||||
"quads",
|
||||
"triceps",
|
||||
"deltoids",
|
||||
"abs",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"upper back"
|
||||
],
|
||||
"equipment": [
|
||||
"Barbell",
|
||||
"Dumbbell",
|
||||
"Plates",
|
||||
"Bench",
|
||||
"Seated Chest Press Machine"
|
||||
],
|
||||
"exercise_count": 31,
|
||||
"created_at": "2023-07-06T15:23:51.228664Z",
|
||||
"updated_at": "2023-07-06T15:23:51.230103Z",
|
||||
"name": "Upper push",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"glutes",
|
||||
"hamstrings",
|
||||
"quads",
|
||||
"feet",
|
||||
"hip flexor",
|
||||
"hip abductors"
|
||||
],
|
||||
"equipment": [
|
||||
"Barbell",
|
||||
"Dumbbell",
|
||||
"Box",
|
||||
"Bench",
|
||||
"Bike"
|
||||
],
|
||||
"exercise_count": 33,
|
||||
"created_at": "2023-07-06T15:39:08.326899Z",
|
||||
"updated_at": "2023-07-06T17:53:45.996684Z",
|
||||
"name": "Lower #1",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"glutes",
|
||||
"quads",
|
||||
"feet",
|
||||
"upper back"
|
||||
],
|
||||
"equipment": [
|
||||
"Dumbbell",
|
||||
"Barbell",
|
||||
"Bench"
|
||||
],
|
||||
"exercise_count": 33,
|
||||
"created_at": "2023-07-06T18:20:27.100936Z",
|
||||
"updated_at": "2023-07-06T18:20:27.102968Z",
|
||||
"name": "Lower 2",
|
||||
"description": "3"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"rotator cuff",
|
||||
"chest",
|
||||
"core",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"triceps",
|
||||
"quads",
|
||||
"abs",
|
||||
"deltoids",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Bench",
|
||||
"Kettlebell",
|
||||
"Battle Ropes"
|
||||
],
|
||||
"exercise_count": 50,
|
||||
"created_at": "2023-07-07T17:18:47.385158Z",
|
||||
"updated_at": "2023-07-07T17:18:47.387277Z",
|
||||
"name": "Cardio",
|
||||
"description": "Ropes, bw squat, kb swings, push up"
|
||||
}
|
||||
]
|
||||
158
iphone/Werkout_ios/JSON/CompletedWorkouts.json
Normal file
158
iphone/Werkout_ios/JSON/CompletedWorkouts.json
Normal file
@@ -0,0 +1,158 @@
|
||||
[
|
||||
{
|
||||
"id": 4,
|
||||
"workout": {
|
||||
"id": 3,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"quads",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower"
|
||||
],
|
||||
"exercise_count": 14,
|
||||
"created_at": "2023-07-03T17:00:01.350058Z",
|
||||
"updated_at": "2023-07-04T16:51:07.231366Z",
|
||||
"name": "Rowwwwwwer",
|
||||
"description": "row until you pass out"
|
||||
},
|
||||
"created_at": "2023-07-03T18:05:58.978336Z",
|
||||
"updated_at": "2023-07-03T18:05:58.985053Z",
|
||||
"difficulty": 3,
|
||||
"total_time": 529,
|
||||
"workout_start_time": "2023-07-03T17:57:01Z",
|
||||
"notes": "",
|
||||
"total_calories": -1.0
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"workout": {
|
||||
"id": 7,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"calves",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"core",
|
||||
"quads",
|
||||
"hip flexor",
|
||||
"middle back",
|
||||
"lats",
|
||||
"lower back",
|
||||
"upper back",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Rower",
|
||||
"Yoga Mat"
|
||||
],
|
||||
"exercise_count": 56,
|
||||
"created_at": "2023-07-05T17:15:16.596202Z",
|
||||
"updated_at": "2023-07-05T17:15:16.597621Z",
|
||||
"name": "Cardio mix #2",
|
||||
"description": ""
|
||||
},
|
||||
"created_at": "2023-07-05T18:00:26.462986Z",
|
||||
"updated_at": "2023-07-05T18:00:26.465537Z",
|
||||
"difficulty": 1,
|
||||
"total_time": 1145,
|
||||
"workout_start_time": "2023-07-05T17:41:05Z",
|
||||
"notes": "",
|
||||
"total_calories": -1.0
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"workout": {
|
||||
"id": 21,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"rotator cuff",
|
||||
"chest",
|
||||
"core",
|
||||
"hamstrings",
|
||||
"glutes",
|
||||
"triceps",
|
||||
"quads",
|
||||
"abs",
|
||||
"deltoids",
|
||||
"biceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Bench",
|
||||
"Kettlebell",
|
||||
"Battle Ropes"
|
||||
],
|
||||
"exercise_count": 50,
|
||||
"created_at": "2023-07-07T17:18:47.385158Z",
|
||||
"updated_at": "2023-07-07T17:18:47.387277Z",
|
||||
"name": "Cardio",
|
||||
"description": "Ropes, bw squat, kb swings, push up"
|
||||
},
|
||||
"created_at": "2023-07-07T17:53:34.629016Z",
|
||||
"updated_at": "2023-07-07T17:53:34.631101Z",
|
||||
"difficulty": 4,
|
||||
"total_time": 1688,
|
||||
"workout_start_time": "2023-07-07T17:25:19Z",
|
||||
"notes": "",
|
||||
"total_calories": -1.0
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"workout": {
|
||||
"id": 11,
|
||||
"registered_user": {
|
||||
"id": 1,
|
||||
"first_name": "User1",
|
||||
"last_name": "user1",
|
||||
"image": "",
|
||||
"nick_name": "test user1"
|
||||
},
|
||||
"muscles": [
|
||||
"chest",
|
||||
"biceps",
|
||||
"forearms",
|
||||
"triceps"
|
||||
],
|
||||
"equipment": [
|
||||
"Dumbbell",
|
||||
"Barbell",
|
||||
"Plates",
|
||||
"Bench"
|
||||
],
|
||||
"exercise_count": 24,
|
||||
"created_at": "2023-07-06T14:56:03.908583Z",
|
||||
"updated_at": "2023-07-06T14:56:03.910350Z",
|
||||
"name": "Arms and HiiT",
|
||||
"description": ""
|
||||
},
|
||||
"created_at": "2023-07-10T19:50:36.188887Z",
|
||||
"updated_at": "2023-07-10T19:50:36.192789Z",
|
||||
"difficulty": 2,
|
||||
"total_time": 1937,
|
||||
"workout_start_time": "2023-07-10T19:18:12Z",
|
||||
"notes": "",
|
||||
"total_calories": 176.9303436279297
|
||||
}
|
||||
]
|
||||
418
iphone/Werkout_ios/JSON/Equipment.json
Normal file
418
iphone/Werkout_ios/JSON/Equipment.json
Normal file
@@ -0,0 +1,418 @@
|
||||
[
|
||||
{
|
||||
"id": 1088,
|
||||
"created_at": "2023-06-11T23:00:16.734003Z",
|
||||
"updated_at": "2023-06-11T23:00:16.734088Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Kettlebell"
|
||||
},
|
||||
{
|
||||
"id": 1089,
|
||||
"created_at": "2023-06-11T23:00:16.736417Z",
|
||||
"updated_at": "2023-06-11T23:00:16.736445Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Yoga Mat"
|
||||
},
|
||||
{
|
||||
"id": 1090,
|
||||
"created_at": "2023-06-11T23:00:16.737296Z",
|
||||
"updated_at": "2023-06-11T23:00:16.737319Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Suspension Trainer"
|
||||
},
|
||||
{
|
||||
"id": 1091,
|
||||
"created_at": "2023-06-11T23:00:16.738300Z",
|
||||
"updated_at": "2023-06-11T23:00:16.738326Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Dumbbell"
|
||||
},
|
||||
{
|
||||
"id": 1092,
|
||||
"created_at": "2023-06-11T23:00:16.739434Z",
|
||||
"updated_at": "2023-06-11T23:00:16.739463Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Box"
|
||||
},
|
||||
{
|
||||
"id": 1093,
|
||||
"created_at": "2023-06-11T23:00:16.743650Z",
|
||||
"updated_at": "2023-06-11T23:00:16.743687Z",
|
||||
"is_weight": false,
|
||||
"category": "Mobility",
|
||||
"name": "Lacrosse Ball"
|
||||
},
|
||||
{
|
||||
"id": 1094,
|
||||
"created_at": "2023-06-11T23:00:16.744876Z",
|
||||
"updated_at": "2023-06-11T23:00:16.744905Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Bench"
|
||||
},
|
||||
{
|
||||
"id": 1095,
|
||||
"created_at": "2023-06-11T23:00:16.746362Z",
|
||||
"updated_at": "2023-06-11T23:00:16.746391Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Barbell"
|
||||
},
|
||||
{
|
||||
"id": 1096,
|
||||
"created_at": "2023-06-11T23:00:16.747581Z",
|
||||
"updated_at": "2023-06-11T23:00:16.747618Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Pull-Up Bar"
|
||||
},
|
||||
{
|
||||
"id": 1097,
|
||||
"created_at": "2023-06-11T23:00:16.748547Z",
|
||||
"updated_at": "2023-06-11T23:00:16.748572Z",
|
||||
"is_weight": true,
|
||||
"category": "",
|
||||
"name": "Plate"
|
||||
},
|
||||
{
|
||||
"id": 1098,
|
||||
"created_at": "2023-06-11T23:00:16.749551Z",
|
||||
"updated_at": "2023-06-11T23:00:16.749584Z",
|
||||
"is_weight": true,
|
||||
"category": "Bands",
|
||||
"name": "Resistance Band"
|
||||
},
|
||||
{
|
||||
"id": 1099,
|
||||
"created_at": "2023-06-11T23:00:16.750466Z",
|
||||
"updated_at": "2023-06-11T23:00:16.750493Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Medicine Ball"
|
||||
},
|
||||
{
|
||||
"id": 1100,
|
||||
"created_at": "2023-06-11T23:00:16.751132Z",
|
||||
"updated_at": "2023-06-11T23:00:16.751148Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Horizontal Leg Press Machine"
|
||||
},
|
||||
{
|
||||
"id": 1101,
|
||||
"created_at": "2023-06-11T23:00:16.751708Z",
|
||||
"updated_at": "2023-06-11T23:00:16.751725Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Battle Ropes"
|
||||
},
|
||||
{
|
||||
"id": 1102,
|
||||
"created_at": "2023-06-11T23:00:16.752239Z",
|
||||
"updated_at": "2023-06-11T23:00:16.752254Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Leg Press Machine"
|
||||
},
|
||||
{
|
||||
"id": 1103,
|
||||
"created_at": "2023-06-11T23:00:16.752770Z",
|
||||
"updated_at": "2023-06-11T23:00:16.752785Z",
|
||||
"is_weight": true,
|
||||
"category": "Other",
|
||||
"name": "Cable Resistance Machine"
|
||||
},
|
||||
{
|
||||
"id": 1104,
|
||||
"created_at": "2023-06-11T23:00:16.753276Z",
|
||||
"updated_at": "2023-06-11T23:00:16.753291Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Partner"
|
||||
},
|
||||
{
|
||||
"id": 1105,
|
||||
"created_at": "2023-06-11T23:00:16.753777Z",
|
||||
"updated_at": "2023-06-11T23:00:16.753791Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Leg Extension Machine"
|
||||
},
|
||||
{
|
||||
"id": 1106,
|
||||
"created_at": "2023-06-11T23:00:16.754288Z",
|
||||
"updated_at": "2023-06-11T23:00:16.754302Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Wall"
|
||||
},
|
||||
{
|
||||
"id": 1107,
|
||||
"created_at": "2023-06-11T23:00:16.754782Z",
|
||||
"updated_at": "2023-06-11T23:00:16.754797Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Plates"
|
||||
},
|
||||
{
|
||||
"id": 1108,
|
||||
"created_at": "2023-06-11T23:00:16.755276Z",
|
||||
"updated_at": "2023-06-11T23:00:16.755291Z",
|
||||
"is_weight": true,
|
||||
"category": "Bands",
|
||||
"name": "Miniband"
|
||||
},
|
||||
{
|
||||
"id": 1109,
|
||||
"created_at": "2023-06-11T23:00:16.755769Z",
|
||||
"updated_at": "2023-06-11T23:00:16.755784Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Agility Ladder"
|
||||
},
|
||||
{
|
||||
"id": 1110,
|
||||
"created_at": "2023-06-11T23:00:16.756257Z",
|
||||
"updated_at": "2023-06-11T23:00:16.756272Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Pec Deck / Reverse Fly Machine"
|
||||
},
|
||||
{
|
||||
"id": 1111,
|
||||
"created_at": "2023-06-11T23:00:16.756790Z",
|
||||
"updated_at": "2023-06-11T23:00:16.756807Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Chest Supported Row Machine"
|
||||
},
|
||||
{
|
||||
"id": 1112,
|
||||
"created_at": "2023-06-11T23:00:16.757376Z",
|
||||
"updated_at": "2023-06-11T23:00:16.757393Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Lat Pull Down Machine"
|
||||
},
|
||||
{
|
||||
"id": 1113,
|
||||
"created_at": "2023-06-11T23:00:16.757953Z",
|
||||
"updated_at": "2023-06-11T23:00:16.757969Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "EZ Bar"
|
||||
},
|
||||
{
|
||||
"id": 1114,
|
||||
"created_at": "2023-06-11T23:00:16.758520Z",
|
||||
"updated_at": "2023-06-11T23:00:16.758536Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Stability Ball"
|
||||
},
|
||||
{
|
||||
"id": 1115,
|
||||
"created_at": "2023-06-11T23:00:16.759018Z",
|
||||
"updated_at": "2023-06-11T23:00:16.759033Z",
|
||||
"is_weight": false,
|
||||
"category": "Mobility",
|
||||
"name": "Dowel"
|
||||
},
|
||||
{
|
||||
"id": 1116,
|
||||
"created_at": "2023-06-11T23:00:16.760471Z",
|
||||
"updated_at": "2023-06-11T23:00:16.760489Z",
|
||||
"is_weight": false,
|
||||
"category": "Mobility",
|
||||
"name": "PVC Pipe"
|
||||
},
|
||||
{
|
||||
"id": 1117,
|
||||
"created_at": "2023-06-11T23:00:16.761033Z",
|
||||
"updated_at": "2023-06-11T23:00:16.761047Z",
|
||||
"is_weight": false,
|
||||
"category": "Mobility",
|
||||
"name": "Foam Roll"
|
||||
},
|
||||
{
|
||||
"id": 1118,
|
||||
"created_at": "2023-06-11T23:00:16.761537Z",
|
||||
"updated_at": "2023-06-11T23:00:16.761552Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Seated Chest Press Machine"
|
||||
},
|
||||
{
|
||||
"id": 1119,
|
||||
"created_at": "2023-06-11T23:00:16.762040Z",
|
||||
"updated_at": "2023-06-11T23:00:16.762055Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Tricep Rope"
|
||||
},
|
||||
{
|
||||
"id": 1120,
|
||||
"created_at": "2023-06-11T23:00:16.762531Z",
|
||||
"updated_at": "2023-06-11T23:00:16.762544Z",
|
||||
"is_weight": false,
|
||||
"category": "Surfaces",
|
||||
"name": "Chair"
|
||||
},
|
||||
{
|
||||
"id": 1121,
|
||||
"created_at": "2023-06-11T23:00:16.763016Z",
|
||||
"updated_at": "2023-06-11T23:00:16.763031Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Seated Shoulder Press Machine"
|
||||
},
|
||||
{
|
||||
"id": 1122,
|
||||
"created_at": "2023-06-11T23:00:16.763508Z",
|
||||
"updated_at": "2023-06-11T23:00:16.763522Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Leg Curl Seated Machine"
|
||||
},
|
||||
{
|
||||
"id": 1123,
|
||||
"created_at": "2023-06-11T23:00:16.763995Z",
|
||||
"updated_at": "2023-06-11T23:00:16.764010Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Seated Calf Raise Machine"
|
||||
},
|
||||
{
|
||||
"id": 1124,
|
||||
"created_at": "2023-06-11T23:00:16.764475Z",
|
||||
"updated_at": "2023-06-11T23:00:16.764488Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Trap Bar"
|
||||
},
|
||||
{
|
||||
"id": 1125,
|
||||
"created_at": "2023-06-11T23:00:16.764897Z",
|
||||
"updated_at": "2023-06-11T23:00:16.764910Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Ab Wheel"
|
||||
},
|
||||
{
|
||||
"id": 1126,
|
||||
"created_at": "2023-06-11T23:00:16.765319Z",
|
||||
"updated_at": "2023-06-11T23:00:16.765332Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Leg Curl Prone Machine"
|
||||
},
|
||||
{
|
||||
"id": 1127,
|
||||
"created_at": "2023-06-11T23:00:16.765741Z",
|
||||
"updated_at": "2023-06-11T23:00:16.765753Z",
|
||||
"is_weight": true,
|
||||
"category": "Weights",
|
||||
"name": "Weighted Vest"
|
||||
},
|
||||
{
|
||||
"id": 1128,
|
||||
"created_at": "2023-06-11T23:00:16.766161Z",
|
||||
"updated_at": "2023-06-11T23:00:16.766173Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Slider"
|
||||
},
|
||||
{
|
||||
"id": 1129,
|
||||
"created_at": "2023-06-11T23:00:16.766577Z",
|
||||
"updated_at": "2023-06-11T23:00:16.766590Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Treadmill"
|
||||
},
|
||||
{
|
||||
"id": 1130,
|
||||
"created_at": "2023-06-11T23:00:16.766998Z",
|
||||
"updated_at": "2023-06-11T23:00:16.767010Z",
|
||||
"is_weight": false,
|
||||
"category": "Other",
|
||||
"name": "Machine"
|
||||
},
|
||||
{
|
||||
"id": 1131,
|
||||
"created_at": "2023-06-11T23:00:16.767415Z",
|
||||
"updated_at": "2023-06-11T23:00:16.767428Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Stair Climber"
|
||||
},
|
||||
{
|
||||
"id": 1132,
|
||||
"created_at": "2023-06-11T23:00:16.767834Z",
|
||||
"updated_at": "2023-06-11T23:00:16.767845Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Assisted Chin up / Dips Machine"
|
||||
},
|
||||
{
|
||||
"id": 1133,
|
||||
"created_at": "2023-06-11T23:00:16.768250Z",
|
||||
"updated_at": "2023-06-11T23:00:16.768261Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Bicep Preacher Curl Machine"
|
||||
},
|
||||
{
|
||||
"id": 1134,
|
||||
"created_at": "2023-06-11T23:00:16.768666Z",
|
||||
"updated_at": "2023-06-11T23:00:16.768678Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Hack Squat Machine"
|
||||
},
|
||||
{
|
||||
"id": 1135,
|
||||
"created_at": "2023-06-11T23:00:16.769086Z",
|
||||
"updated_at": "2023-06-11T23:00:16.769098Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Jump Rope"
|
||||
},
|
||||
{
|
||||
"id": 1136,
|
||||
"created_at": "2023-06-11T23:00:16.769515Z",
|
||||
"updated_at": "2023-06-11T23:00:16.769527Z",
|
||||
"is_weight": true,
|
||||
"category": "Machine",
|
||||
"name": "Hack Squat 45 Degree Machine"
|
||||
},
|
||||
{
|
||||
"id": 1137,
|
||||
"created_at": "2023-06-11T23:00:16.770084Z",
|
||||
"updated_at": "2023-06-11T23:00:16.770096Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Bike"
|
||||
},
|
||||
{
|
||||
"id": 1138,
|
||||
"created_at": "2023-06-11T23:00:16.770500Z",
|
||||
"updated_at": "2023-06-11T23:00:16.770512Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Rower"
|
||||
},
|
||||
{
|
||||
"id": 1139,
|
||||
"created_at": "2023-06-11T23:00:16.770915Z",
|
||||
"updated_at": "2023-06-11T23:00:16.770928Z",
|
||||
"is_weight": false,
|
||||
"category": "Cardio",
|
||||
"name": "Elliptical"
|
||||
}
|
||||
]
|
||||
53422
iphone/Werkout_ios/JSON/Exercises.json
Normal file
53422
iphone/Werkout_ios/JSON/Exercises.json
Normal file
File diff suppressed because it is too large
Load Diff
16
iphone/Werkout_ios/JSON/PlannedWorkouts.json
Normal file
16
iphone/Werkout_ios/JSON/PlannedWorkouts.json
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": "2023-07-02T04:16:34.262272Z",
|
||||
"updated_at": "2023-07-02T04:16:34.262315Z",
|
||||
"on_date": "2023-07-02",
|
||||
"workout": {
|
||||
"id": 22,
|
||||
"created_at": "2023-06-26T02:53:56.101972Z",
|
||||
"updated_at": "2023-06-26T02:53:56.106136Z",
|
||||
"name": "Aaaa",
|
||||
"description": "description",
|
||||
"registered_user": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
107
iphone/Werkout_ios/JSON/PreviewData.swift
Normal file
107
iphone/Werkout_ios/JSON/PreviewData.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// PreviewData.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PreviewData {
|
||||
class func workout() -> Workout {
|
||||
let filepath = Bundle.main.path(forResource: "WorkoutDetail", ofType: "json")!
|
||||
let data = try! Data(NSData(contentsOfFile: filepath))
|
||||
let workout = try! JSONDecoder().decode(Workout.self, from: data)
|
||||
return workout
|
||||
}
|
||||
|
||||
class func allWorkouts() -> [Workout] {
|
||||
if let filepath = Bundle.main.path(forResource: "AllWorkouts", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let workout = try JSONDecoder().decode([Workout].self, from: data)
|
||||
return workout
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
class func parseExercises() -> [Exercise] {
|
||||
if let filepath = Bundle.main.path(forResource: "Exercises", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let exercises = try JSONDecoder().decode([Exercise].self, from: data)
|
||||
return exercises
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
class func parseEquipment() -> [Equipment] {
|
||||
if let filepath = Bundle.main.path(forResource: "Equipment", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let equipment = try JSONDecoder().decode([Equipment].self, from: data)
|
||||
return equipment
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
class func parseMuscle() -> [Muscle] {
|
||||
if let filepath = Bundle.main.path(forResource: "AllMuscles", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let muscles = try JSONDecoder().decode([Muscle].self, from: data)
|
||||
return muscles
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
class func parseRegisterdUser() -> RegisteredUser {
|
||||
if let filepath = Bundle.main.path(forResource: "RegisteredUser", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let muscles = try JSONDecoder().decode(RegisteredUser.self, from: data)
|
||||
return muscles
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
class func parseCompletedWorkouts() -> [CompletedWorkout] {
|
||||
if let filepath = Bundle.main.path(forResource: "CompletedWorkouts", ofType: "json") {
|
||||
do {
|
||||
let data = try Data(NSData(contentsOfFile: filepath))
|
||||
let muscles = try JSONDecoder().decode([CompletedWorkout].self, from: data)
|
||||
return muscles
|
||||
} catch {
|
||||
print(error)
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
11
iphone/Werkout_ios/JSON/RegisteredUser.json
Normal file
11
iphone/Werkout_ios/JSON/RegisteredUser.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": 1,
|
||||
"email_address": "user1@user1.com",
|
||||
"created_at": "2023-06-11T22:09:52.419314Z",
|
||||
"updated_at": "2023-06-11T22:09:52.419342Z",
|
||||
"first_name": "test1_fist",
|
||||
"last_name": "test1_last",
|
||||
"image": "",
|
||||
"nick_name": "NickkkkName",
|
||||
"token": "8f10a5b8c7532f7f8602193767b46a2625a85c52"
|
||||
}
|
||||
221
iphone/Werkout_ios/JSON/WorkoutDetail.json
Normal file
221
iphone/Werkout_ios/JSON/WorkoutDetail.json
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Ipad",
|
||||
"description": "description",
|
||||
"supersets": [
|
||||
{
|
||||
"id": 1,
|
||||
"exercises": [
|
||||
{
|
||||
"id": 1,
|
||||
"exercise": {
|
||||
"id": 520,
|
||||
"muscles": [
|
||||
{
|
||||
"id": 7264,
|
||||
"name": "hip flexor",
|
||||
"created_at": "2023-06-14T17:05:39.760515Z",
|
||||
"updated_at": "2023-06-14T17:05:39.761372Z",
|
||||
"exercise": 520,
|
||||
"muscle": 16
|
||||
},
|
||||
{
|
||||
"id": 7265,
|
||||
"name": "glutes",
|
||||
"created_at": "2023-06-14T17:05:39.762342Z",
|
||||
"updated_at": "2023-06-14T17:05:39.762814Z",
|
||||
"exercise": 520,
|
||||
"muscle": 4
|
||||
}
|
||||
],
|
||||
"equipment": [
|
||||
{
|
||||
"id": 941,
|
||||
"name": "Wall",
|
||||
"created_at": "2023-06-13T02:28:04.289213Z",
|
||||
"updated_at": "2023-06-13T02:28:04.290354Z",
|
||||
"exercise": 520,
|
||||
"equipment": 1106
|
||||
}
|
||||
],
|
||||
"audio_url": "exercise_audio/1-Step_Wall_March.m4a",
|
||||
"video_url": "exercise_videos/1-Step_Wall_March.mp4",
|
||||
"created_at": "2023-06-11T22:50:19.020826Z",
|
||||
"updated_at": "2023-06-11T22:50:19.020834Z",
|
||||
"name": "1-Step Wall March",
|
||||
"description": "Keeping a tall posture, lean forward with both arms straight ahead, palms on the wall. While pushing your toes back through the floor, drive one knee up towards your ribs. Pause, then quickly repeat the same movement with the opposite leg. ",
|
||||
"side": "",
|
||||
"is_two_dumbbells": false,
|
||||
"is_trackable_distance": false,
|
||||
"is_alternating": true,
|
||||
"is_weight": true,
|
||||
"is_distance": false,
|
||||
"is_duration": true,
|
||||
"is_reps": true,
|
||||
"joints_used": "ankle,knee,hip,wrist,shoulder,elbow",
|
||||
"movement_patterns": "plyometric",
|
||||
"equipment_required": "Wall",
|
||||
"muscle_groups": "hip flexor,glutes",
|
||||
"synonyms": ""
|
||||
},
|
||||
"created_at": "2023-07-17T18:56:36.984049Z",
|
||||
"updated_at": "2023-07-17T19:06:00.534838Z",
|
||||
"weight": null,
|
||||
"reps": null,
|
||||
"duration": 30,
|
||||
"order": 1,
|
||||
"superset": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"exercise": {
|
||||
"id": 992,
|
||||
"muscles": [
|
||||
{
|
||||
"id": 7270,
|
||||
"name": "hamstrings",
|
||||
"created_at": "2023-06-14T17:05:39.769351Z",
|
||||
"updated_at": "2023-06-14T17:05:39.769758Z",
|
||||
"exercise": 992,
|
||||
"muscle": 6
|
||||
},
|
||||
{
|
||||
"id": 7271,
|
||||
"name": "glutes",
|
||||
"created_at": "2023-06-14T17:05:39.770480Z",
|
||||
"updated_at": "2023-06-14T17:05:39.771111Z",
|
||||
"exercise": 992,
|
||||
"muscle": 4
|
||||
}
|
||||
],
|
||||
"equipment": [
|
||||
{
|
||||
"id": 944,
|
||||
"name": "Dumbbell",
|
||||
"created_at": "2023-06-13T02:28:04.294180Z",
|
||||
"updated_at": "2023-06-13T02:28:04.294658Z",
|
||||
"exercise": 992,
|
||||
"equipment": 1091
|
||||
}
|
||||
],
|
||||
"audio_url": "exercise_audio/2_Dumbbell_Single-Leg_Deadlift.m4a",
|
||||
"video_url": "exercise_videos/2_Dumbbell_Single-Leg_Deadlift.mp4",
|
||||
"created_at": "2023-06-11T22:50:19.197099Z",
|
||||
"updated_at": "2023-06-11T22:50:19.197105Z",
|
||||
"name": "2 Dumbbell Single-Leg Deadlift",
|
||||
"description": "Holding a dumbbell in each hand, with your right leg on the ground, hinge at your hips and let your body see-saw down until you are parallel with the ground. Snap back to a standing position.",
|
||||
"side": "right_leg",
|
||||
"is_two_dumbbells": true,
|
||||
"is_trackable_distance": false,
|
||||
"is_alternating": false,
|
||||
"is_weight": true,
|
||||
"is_distance": false,
|
||||
"is_duration": true,
|
||||
"is_reps": true,
|
||||
"joints_used": "ankle,lumbar spine,hip,knee,wrist",
|
||||
"movement_patterns": "lower pull,lower pull - hip hinge",
|
||||
"equipment_required": "Dumbbell",
|
||||
"muscle_groups": "hamstrings,glutes",
|
||||
"synonyms": "2 Dumbbell Single Leg Deadlift"
|
||||
},
|
||||
"created_at": "2023-07-17T18:56:36.984523Z",
|
||||
"updated_at": "2023-07-17T19:06:00.535334Z",
|
||||
"weight": 30,
|
||||
"reps": null,
|
||||
"duration": null,
|
||||
"order": 2,
|
||||
"superset": 1
|
||||
}
|
||||
],
|
||||
"created_at": "2023-07-17T18:56:36.983159Z",
|
||||
"updated_at": "2023-07-17T19:06:00.534027Z",
|
||||
"name": "test superste",
|
||||
"rounds": 3,
|
||||
"order": 1,
|
||||
"workout": 21
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"exercises": [
|
||||
{
|
||||
"id": 3,
|
||||
"exercise": {
|
||||
"id": 992,
|
||||
"muscles": [
|
||||
{
|
||||
"id": 7270,
|
||||
"name": "hamstrings",
|
||||
"created_at": "2023-06-14T17:05:39.769351Z",
|
||||
"updated_at": "2023-06-14T17:05:39.769758Z",
|
||||
"exercise": 992,
|
||||
"muscle": 6
|
||||
},
|
||||
{
|
||||
"id": 7271,
|
||||
"name": "glutes",
|
||||
"created_at": "2023-06-14T17:05:39.770480Z",
|
||||
"updated_at": "2023-06-14T17:05:39.771111Z",
|
||||
"exercise": 992,
|
||||
"muscle": 4
|
||||
}
|
||||
],
|
||||
"equipment": [
|
||||
{
|
||||
"id": 944,
|
||||
"name": "Dumbbell",
|
||||
"created_at": "2023-06-13T02:28:04.294180Z",
|
||||
"updated_at": "2023-06-13T02:28:04.294658Z",
|
||||
"exercise": 992,
|
||||
"equipment": 1091
|
||||
}
|
||||
],
|
||||
"audio_url": "exercise_audio/2_Dumbbell_Single-Leg_Deadlift.m4a",
|
||||
"video_url": "exercise_videos/2_Dumbbell_Single-Leg_Deadlift.mp4",
|
||||
"created_at": "2023-06-11T22:50:19.197099Z",
|
||||
"updated_at": "2023-06-11T22:50:19.197105Z",
|
||||
"name": "2 Dumbbell Single-Leg Deadlift",
|
||||
"description": "Holding a dumbbell in each hand, with your right leg on the ground, hinge at your hips and let your body see-saw down until you are parallel with the ground. Snap back to a standing position.",
|
||||
"side": "right_leg",
|
||||
"is_two_dumbbells": true,
|
||||
"is_trackable_distance": false,
|
||||
"is_alternating": false,
|
||||
"is_weight": true,
|
||||
"is_distance": false,
|
||||
"is_duration": true,
|
||||
"is_reps": true,
|
||||
"joints_used": "ankle,lumbar spine,hip,knee,wrist",
|
||||
"movement_patterns": "lower pull,lower pull - hip hinge",
|
||||
"equipment_required": "Dumbbell",
|
||||
"muscle_groups": "hamstrings,glutes",
|
||||
"synonyms": "2 Dumbbell Single Leg Deadlift"
|
||||
},
|
||||
"created_at": "2023-07-17T18:58:35.585418Z",
|
||||
"updated_at": "2023-07-17T18:58:35.585435Z",
|
||||
"weight": 11,
|
||||
"reps": null,
|
||||
"duration": null,
|
||||
"order": 1,
|
||||
"superset": 2
|
||||
}
|
||||
],
|
||||
"created_at": "2023-07-17T18:58:35.584036Z",
|
||||
"updated_at": "2023-07-17T19:03:57.175639Z",
|
||||
"name": "two",
|
||||
"rounds": 3,
|
||||
"order": 2,
|
||||
"workout": 21
|
||||
}
|
||||
],
|
||||
"registered_user": {
|
||||
"id": 2,
|
||||
"first_name": "test2_first",
|
||||
"last_name": "test2_last",
|
||||
"image": "",
|
||||
"nick_name": null
|
||||
},
|
||||
"male_videos": [],
|
||||
"female_videos": [
|
||||
"videos/Recover_8.mp4"
|
||||
],
|
||||
"both_videos": []
|
||||
}
|
||||
172
iphone/Werkout_ios/Keychain.swift
Normal file
172
iphone/Werkout_ios/Keychain.swift
Normal file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Keychain.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class KeychainInterface {
|
||||
enum KeychainError: Error {
|
||||
// Attempted read for an item that does not exist.
|
||||
case itemNotFound
|
||||
|
||||
// Attempted save to override an existing item.
|
||||
// Use update instead of save to update existing items
|
||||
case duplicateItem
|
||||
|
||||
// A read of an item in any format other than Data
|
||||
case invalidItemFormat
|
||||
|
||||
// Any operation result status than errSecSuccess
|
||||
case unexpectedStatus(OSStatus)
|
||||
}
|
||||
|
||||
static let serviceID = "werkout.fitness"
|
||||
|
||||
static func save(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to save in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
|
||||
// kSecValueData is the item value to save
|
||||
kSecValueData as String: password as AnyObject
|
||||
]
|
||||
|
||||
// SecItemAdd attempts to add the item identified by
|
||||
// the query to keychain
|
||||
let status = SecItemAdd(
|
||||
query as CFDictionary,
|
||||
nil
|
||||
)
|
||||
|
||||
// errSecDuplicateItem is a special case where the
|
||||
// item identified by the query already exists. Throw
|
||||
// duplicateItem so the client can determine whether
|
||||
// or not to handle this as an error
|
||||
if status == errSecDuplicateItem {
|
||||
throw KeychainError.duplicateItem
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// save operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
static func update(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to update in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
|
||||
// attributes is passed to SecItemUpdate with
|
||||
// kSecValueData as the updated item value
|
||||
let attributes: [String: AnyObject] = [
|
||||
kSecValueData as String: password as AnyObject
|
||||
]
|
||||
|
||||
// SecItemUpdate attempts to update the item identified
|
||||
// by query, overriding the previous value
|
||||
let status = SecItemUpdate(
|
||||
query as CFDictionary,
|
||||
attributes as CFDictionary
|
||||
)
|
||||
|
||||
// errSecItemNotFound is a special status indicating the
|
||||
// item to update does not exist. Throw itemNotFound so
|
||||
// the client can determine whether or not to handle
|
||||
// this as an error
|
||||
guard status != errSecItemNotFound else {
|
||||
throw KeychainError.itemNotFound
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// update operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
static func readPassword(service: String = KeychainInterface.serviceID, account: String) throws -> Data {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to read in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
|
||||
// kSecMatchLimitOne indicates keychain should read
|
||||
// only the most recent item matching this query
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
|
||||
// kSecReturnData is set to kCFBooleanTrue in order
|
||||
// to retrieve the data for the item
|
||||
kSecReturnData as String: kCFBooleanTrue
|
||||
]
|
||||
|
||||
// SecItemCopyMatching will attempt to copy the item
|
||||
// identified by query to the reference itemCopy
|
||||
var itemCopy: AnyObject?
|
||||
let status = SecItemCopyMatching(
|
||||
query as CFDictionary,
|
||||
&itemCopy
|
||||
)
|
||||
|
||||
// errSecItemNotFound is a special status indicating the
|
||||
// read item does not exist. Throw itemNotFound so the
|
||||
// client can determine whether or not to handle
|
||||
// this case
|
||||
guard status != errSecItemNotFound else {
|
||||
throw KeychainError.itemNotFound
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// read operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
|
||||
// This implementation of KeychainInterface requires all
|
||||
// items to be saved and read as Data. Otherwise,
|
||||
// invalidItemFormat is thrown
|
||||
guard let password = itemCopy as? Data else {
|
||||
throw KeychainError.invalidItemFormat
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
static func deletePassword(service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to delete in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
|
||||
// SecItemDelete attempts to perform a delete operation
|
||||
// for the item identified by query. The status indicates
|
||||
// if the operation succeeded or failed.
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// delete operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
iphone/Werkout_ios/Network/Fetchables.swift
Normal file
109
iphone/Werkout_ios/Network/Fetchables.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// AllWorkoutFetchable.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/19/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AllWorkoutFetchable: Fetchable {
|
||||
typealias Response = [Workout]
|
||||
var endPoint: String = "/workout/all/"
|
||||
}
|
||||
|
||||
class WorkoutDetailFetchable: Fetchable {
|
||||
typealias Response = Workout
|
||||
var endPoint: String
|
||||
|
||||
init(workoutID: Int) {
|
||||
self.endPoint = "/workout/"+String(workoutID)+"/details/"
|
||||
}
|
||||
}
|
||||
|
||||
class AllMusclesFetchable: Fetchable {
|
||||
typealias Response = [Muscle]
|
||||
var endPoint: String = "/muscle/all/"
|
||||
}
|
||||
|
||||
class AllEquipmentFetchable: Fetchable {
|
||||
typealias Response = [Equipment]
|
||||
var endPoint: String = "/equipment/all/"
|
||||
}
|
||||
|
||||
class AllExerciseFetchable: Fetchable {
|
||||
typealias Response = [Exercise]
|
||||
var endPoint: String = "/exercise/all/"
|
||||
}
|
||||
|
||||
class CompletedWorkoutFetchable: Fetchable {
|
||||
typealias Response = [CompletedWorkout]
|
||||
var endPoint: String = "/workout/completed/"
|
||||
}
|
||||
|
||||
class PlannedWorkoutFetchable: Fetchable {
|
||||
typealias Response = [PlannedWorkout]
|
||||
var endPoint: String = "/workout/planned_workouts/"
|
||||
}
|
||||
|
||||
class CreateWorkoutFetchable: Postable {
|
||||
var postableData: [String : Any]?
|
||||
var successStatus = 201
|
||||
|
||||
typealias Response = Workout
|
||||
var endPoint: String = "/workout/create/"
|
||||
|
||||
init(postData: [String: Any]) {
|
||||
self.postableData = postData
|
||||
}
|
||||
}
|
||||
|
||||
class CompleteWorkoutFetchable: Postable {
|
||||
var postableData: [String : Any]?
|
||||
var successStatus = 201
|
||||
|
||||
typealias Response = CompletedWorkoutPOSTReturn
|
||||
var endPoint: String = "/workout/complete/"
|
||||
|
||||
init(postData: [String: Any]) {
|
||||
self.postableData = postData
|
||||
}
|
||||
}
|
||||
|
||||
class LoginFetchable: Postable {
|
||||
var postableData: [String : Any]?
|
||||
var successStatus = 200
|
||||
|
||||
typealias Response = RegisteredUser
|
||||
var endPoint: String = "/registered_user/login/"
|
||||
|
||||
var attachToken: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
init(postData: [String: Any]) {
|
||||
self.postableData = postData
|
||||
}
|
||||
}
|
||||
|
||||
class PlanWorkoutFetchable: Postable {
|
||||
var postableData: [String : Any]?
|
||||
var successStatus = 201
|
||||
|
||||
typealias Response = RegisteredUser
|
||||
var endPoint: String = "/workout/plan_workout/"
|
||||
|
||||
init(postData: [String: Any]) {
|
||||
self.postableData = postData
|
||||
}
|
||||
}
|
||||
|
||||
class AllNSFWVideosFetchable: Fetchable {
|
||||
typealias Response = [NSFWVideo]
|
||||
var endPoint: String = "/videos/nsfw_videos/"
|
||||
}
|
||||
|
||||
class RefreshUserInfoFetcable: Fetchable {
|
||||
typealias Response = RegisteredUser
|
||||
var endPoint: String = "/registered_user/refresh/"
|
||||
}
|
||||
138
iphone/Werkout_ios/Network/Network.swift
Normal file
138
iphone/Werkout_ios/Network/Network.swift
Normal file
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// Network.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/19/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FetchableError: Error {
|
||||
case apiError(Error)
|
||||
case noData
|
||||
case decodeError(Error)
|
||||
case endOfFileError
|
||||
case noPostData
|
||||
case noToken
|
||||
case statusError(Int, String?)
|
||||
}
|
||||
|
||||
protocol Fetchable {
|
||||
associatedtype Response: Codable
|
||||
var attachToken: Bool { get }
|
||||
var baseURL: String { get }
|
||||
var endPoint: String { get }
|
||||
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void)
|
||||
}
|
||||
|
||||
protocol Postable: Fetchable {
|
||||
var postableData: [String: Any]? { get }
|
||||
var successStatus: Int { get }
|
||||
}
|
||||
|
||||
extension Fetchable {
|
||||
var baseURL: String {
|
||||
BaseURLs.currentBaseURL
|
||||
}
|
||||
|
||||
var attachToken: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void) {
|
||||
let url = URL(string: baseURL+endPoint)!
|
||||
|
||||
var request = URLRequest(url: url,timeoutInterval: Double.infinity)
|
||||
if attachToken {
|
||||
guard let token = UserStore.shared.token else {
|
||||
completion(.failure(.noPostData))
|
||||
return
|
||||
}
|
||||
request.addValue(token, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
if let error = error {
|
||||
completion(.failure(.apiError(error)))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.failure(.noData))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let model = try JSONDecoder().decode(Response.self, from: data)
|
||||
completion(.success(model))
|
||||
return
|
||||
} catch {
|
||||
completion(.failure(.decodeError(error)))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension Postable {
|
||||
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void) {
|
||||
guard let postableData = postableData else {
|
||||
completion(.failure(.noPostData))
|
||||
return
|
||||
}
|
||||
|
||||
let url = URL(string: baseURL+endPoint)!
|
||||
|
||||
let postData = try! JSONSerialization.data(withJSONObject:postableData)
|
||||
|
||||
var request = URLRequest(url: url,timeoutInterval: Double.infinity)
|
||||
if attachToken {
|
||||
guard let token = UserStore.shared.token else {
|
||||
completion(.failure(.noPostData))
|
||||
return
|
||||
}
|
||||
request.addValue(token, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = postData
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(.apiError(error)))
|
||||
return
|
||||
}
|
||||
|
||||
if let httpRespone = response as? HTTPURLResponse {
|
||||
if httpRespone.statusCode != successStatus {
|
||||
var returnStr: String?
|
||||
if let data = data {
|
||||
returnStr = String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
completion(.failure(.statusError(httpRespone.statusCode, returnStr)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(.failure(.noData))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let model = try JSONDecoder().decode(Response.self, from: data)
|
||||
completion(.success(model))
|
||||
return
|
||||
} catch {
|
||||
completion(.failure(.decodeError(error)))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
56
iphone/Werkout_ios/Persistence.swift
Normal file
56
iphone/Werkout_ios/Persistence.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Persistence.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/13/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
struct PersistenceController {
|
||||
static let shared = PersistenceController()
|
||||
|
||||
static var preview: PersistenceController = {
|
||||
let result = PersistenceController(inMemory: true)
|
||||
let viewContext = result.container.viewContext
|
||||
for _ in 0..<10 {
|
||||
let newItem = Item(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
}
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
let container: NSPersistentContainer
|
||||
|
||||
init(inMemory: Bool = false) {
|
||||
container = NSPersistentContainer(name: "Werkout_ios")
|
||||
if inMemory {
|
||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
/*
|
||||
Typical reasons for an error here include:
|
||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||
* The device is out of space.
|
||||
* The store could not be migrated to the current model version.
|
||||
Check the error message to determine what the actual problem was.
|
||||
*/
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
})
|
||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iphone/Werkout_ios/Preview Content/Straight_Leg_Sit_Up.mp4
Normal file
BIN
iphone/Werkout_ios/Preview Content/Straight_Leg_Sit_Up.mp4
Normal file
Binary file not shown.
31
iphone/Werkout_ios/ThotStyle.swift
Normal file
31
iphone/Werkout_ios/ThotStyle.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// ThotStyle.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/13/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ThotStyle: Int, CaseIterable {
|
||||
case always = 1
|
||||
case never = 2
|
||||
case recovery = 3
|
||||
case random = 4
|
||||
case off
|
||||
|
||||
func stringValue() -> String {
|
||||
switch(self) {
|
||||
case .always:
|
||||
return "Always"
|
||||
case .never:
|
||||
return "Never"
|
||||
case .recovery:
|
||||
return "Recovery"
|
||||
case .random:
|
||||
return "Random"
|
||||
case .off:
|
||||
return "Off"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
iphone/Werkout_ios/ToDo
Normal file
12
iphone/Werkout_ios/ToDo
Normal file
@@ -0,0 +1,12 @@
|
||||
-completed workout view
|
||||
- add notes and slider for difficulty
|
||||
|
||||
-apple watch
|
||||
|
||||
-account view
|
||||
workout history view
|
||||
-calorie upload
|
||||
|
||||
-video view on iphone during workout
|
||||
|
||||
edit weights on workouts
|
||||
116
iphone/Werkout_ios/UserStore.swift
Normal file
116
iphone/Werkout_ios/UserStore.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// UserStore.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/25/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UserStore: ObservableObject {
|
||||
static let userNameKeychainValue = "username"
|
||||
static let passwordKeychainValue = "password"
|
||||
|
||||
static let userDefaultsRegisteredUserKey = "registeredUserKey"
|
||||
static let shared = UserStore()
|
||||
|
||||
@Published public private(set) var registeredUser: RegisteredUser?
|
||||
|
||||
var plannedWorkouts = [PlannedWorkout]()
|
||||
|
||||
init(registeredUser: RegisteredUser? = nil) {
|
||||
self.registeredUser = registeredUser
|
||||
if let data = UserDefaults.standard.data(forKey: UserStore.userDefaultsRegisteredUserKey),
|
||||
let model = try? JSONDecoder().decode(RegisteredUser.self, from: data) {
|
||||
self.registeredUser = model
|
||||
}
|
||||
}
|
||||
|
||||
public var token: String? {
|
||||
guard let token = registeredUser?.token else {
|
||||
return nil
|
||||
}
|
||||
return "Token \(token)"
|
||||
}
|
||||
|
||||
func login(postData: [String: Any], completion: @escaping (Bool)-> Void) {
|
||||
LoginFetchable(postData: postData).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
if let email = postData["email"] as? String,
|
||||
let password = postData["password"] as? String,
|
||||
let data = password.data(using: .utf8) {
|
||||
try? KeychainInterface.save(password: data,
|
||||
account: email)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.registeredUser = model
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
UserDefaults.standard.set(data, forKey: UserStore.userDefaultsRegisteredUserKey)
|
||||
completion(true)
|
||||
}
|
||||
case .failure(let failure):
|
||||
completion(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func refreshUserData() {
|
||||
RefreshUserInfoFetcable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let registeredUser):
|
||||
DispatchQueue.main.async {
|
||||
if let data = try? JSONEncoder().encode(registeredUser) {
|
||||
UserDefaults.standard.set(data, forKey: UserStore.userDefaultsRegisteredUserKey)
|
||||
}
|
||||
if let data = UserDefaults.standard.data(forKey: UserStore.userDefaultsRegisteredUserKey),
|
||||
let model = try? JSONDecoder().decode(RegisteredUser.self, from: data) {
|
||||
self.registeredUser = model
|
||||
}
|
||||
}
|
||||
case .failure(let failure):
|
||||
fatalError()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func logout() {
|
||||
self.registeredUser = nil
|
||||
UserDefaults.standard.set(nil, forKey: UserStore.userDefaultsRegisteredUserKey)
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: NSNotification.Name("CreatedNewWorkout"), object: nil, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func setFakeUser() {
|
||||
self.registeredUser = PreviewData.parseRegisterdUser()
|
||||
}
|
||||
|
||||
func fetchPlannedWorkouts() {
|
||||
PlannedWorkoutFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let models):
|
||||
self.plannedWorkouts = models
|
||||
case .failure(let failure):
|
||||
UserStore.shared.logout()
|
||||
// fatalError("shit broke")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func plannedWorkoutFor(date: Date) -> PlannedWorkout? {
|
||||
for plannedWorkout in plannedWorkouts {
|
||||
if let plannedworkoutDate = plannedWorkout.date {
|
||||
if Calendar.current.isDate(date, equalTo: plannedworkoutDate, toGranularity: .day) {
|
||||
return plannedWorkout
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTreyDevRegisterdUser() {
|
||||
self.registeredUser = RegisteredUser(id: 1, firstName: "t", lastName: "t", image: nil, nickName: "t", token: "15d7565cde9e8c904ae934f8235f68f6a24b4a03", email: nil, hasNSFWToggle: nil)
|
||||
}
|
||||
}
|
||||
45
iphone/Werkout_ios/Views/AccountView/AccountView.swift
Normal file
45
iphone/Werkout_ios/Views/AccountView/AccountView.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// AccountView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct AccountView: View {
|
||||
@ObservedObject var userStore = UserStore.shared
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
NameView()
|
||||
|
||||
CompletedWorkoutsView()
|
||||
|
||||
Divider()
|
||||
|
||||
ThotPreferenceView()
|
||||
|
||||
ShowNextUpView()
|
||||
|
||||
Spacer()
|
||||
|
||||
Logoutview()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
//struct AccountView_Previews: PreviewProvider {
|
||||
// static let userStore = UserStore.shared
|
||||
// static let completedWorkouts = PreviewData.parseCompletedWorkouts()
|
||||
//
|
||||
// static var previews: some View {
|
||||
// AccountView(completedWorkouts: completedWorkouts)
|
||||
// .onAppear{
|
||||
// userStore.setFakeUser()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
95
iphone/Werkout_ios/Views/AddExercise/AddExerciseView.swift
Normal file
95
iphone/Werkout_ios/Views/AddExercise/AddExerciseView.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// CreateWorkout.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct AddExerciseView: View {
|
||||
enum CreateWorkoutItemPickerViewType {
|
||||
case muscles
|
||||
case equipment
|
||||
}
|
||||
|
||||
@State var selectedMuscles = [Muscle]()
|
||||
@State var selectedEquipment = [Equipment]()
|
||||
@State var filteredExercises = [Exercise]()
|
||||
|
||||
@StateObject var bridgeModule = BridgeModule.shared
|
||||
let selectedExercise: ((Exercise) -> Void)
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
AllExerciseView(filteredExercises: $filteredExercises,
|
||||
selectedExercise: { excercise in
|
||||
selectedExercise(excercise)
|
||||
})
|
||||
.padding(.top)
|
||||
|
||||
HStack {
|
||||
AllMusclesView(selectedMuscles: $selectedMuscles)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Divider()
|
||||
|
||||
AllEquipmentView(selectedEquipment: $selectedEquipment)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.top)
|
||||
.frame(height: 44)
|
||||
}
|
||||
.onChange(of: selectedMuscles, perform: { _ in
|
||||
filterExercises()
|
||||
}) .onChange(of: selectedEquipment, perform: { _ in
|
||||
filterExercises()
|
||||
})
|
||||
}
|
||||
|
||||
func filterExercises() {
|
||||
guard let exercises = DataStore.shared.allExercise else {
|
||||
filteredExercises = []
|
||||
return
|
||||
}
|
||||
|
||||
let filtered = exercises.filter({ exercise in
|
||||
var hasCorrectMuscles = false
|
||||
if selectedMuscles.count == 0 {
|
||||
hasCorrectMuscles = true
|
||||
} else {
|
||||
let exerciseMuscleIds = exercise.muscles.map({ $0.muscle ?? -1 })
|
||||
let selctedMuscleIds = selectedMuscles.map({ $0.id })
|
||||
// if one items match
|
||||
if exerciseMuscleIds.contains(where: selctedMuscleIds.contains) {
|
||||
// if all items match
|
||||
hasCorrectMuscles = true
|
||||
}
|
||||
}
|
||||
|
||||
var hasCorrectEquipment = false
|
||||
if selectedEquipment.count == 0 {
|
||||
hasCorrectEquipment = true
|
||||
} else {
|
||||
let exerciseEquipmentIds = exercise.equipment.map({ $0.equipment ?? -1 })
|
||||
let selctedEquipmentIds = selectedEquipment.map({ $0.id })
|
||||
// if one items match
|
||||
if exerciseEquipmentIds.contains(where: selctedEquipmentIds.contains) {
|
||||
// if all items match
|
||||
hasCorrectEquipment = true
|
||||
}
|
||||
}
|
||||
return hasCorrectMuscles && hasCorrectEquipment
|
||||
})
|
||||
|
||||
filteredExercises = filtered
|
||||
}
|
||||
}
|
||||
|
||||
//struct AddExerciseView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AddExerciseView(selectedExercise: { _ in })
|
||||
// }
|
||||
//}
|
||||
161
iphone/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift
Normal file
161
iphone/Werkout_ios/Views/AllWorkouts/AllWorkoutsListView.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// AllWorkoutsListView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllWorkoutsListView: View {
|
||||
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
|
||||
WorkoutOverviewView(workout: workout)
|
||||
.padding([.leading, .trailing])
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedWorkout(workout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
refresh()
|
||||
}
|
||||
|
||||
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(uniqueWorkoutUsers: .constant([]),
|
||||
workouts: PreviewData.allWorkouts(),
|
||||
selectedWorkout: { workout in },
|
||||
refresh: { })
|
||||
}
|
||||
}
|
||||
204
iphone/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift
Normal file
204
iphone/Werkout_ios/Views/AllWorkouts/AllWorkoutsView.swift
Normal file
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// AllWorkoutsView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import HealthKit
|
||||
|
||||
enum MainViewTypes: Int, CaseIterable {
|
||||
case AllWorkout = 0
|
||||
case MyWorkouts
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
|
||||
case .AllWorkout:
|
||||
return "All Workouts"
|
||||
case .MyWorkouts:
|
||||
return "Planned Workouts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AllWorkoutsView: View {
|
||||
@State var isUpdating = false
|
||||
@State var workouts: [Workout]?
|
||||
@State var uniqueWorkoutUsers: [RegisteredUser]?
|
||||
let healthStore = HKHealthStore()
|
||||
var bridgeModule = BridgeModule.shared
|
||||
@State public var needsUpdating: Bool = true
|
||||
|
||||
@ObservedObject var dataStore = DataStore.shared
|
||||
|
||||
@State private var showWorkoutDetail = false
|
||||
@State private var selectedWorkout: Workout? {
|
||||
didSet {
|
||||
bridgeModule.currentExerciseInfo.workout = selectedWorkout
|
||||
}
|
||||
}
|
||||
|
||||
@State private var selectedPlannedWorkout: Workout? {
|
||||
didSet {
|
||||
bridgeModule.currentExerciseInfo.workout = selectedPlannedWorkout
|
||||
}
|
||||
}
|
||||
|
||||
@State private var showLoginView = false
|
||||
@State private var selectedSegment: MainViewTypes = .AllWorkout
|
||||
@State var selectedDate: Date = Date()
|
||||
|
||||
let pub = NotificationCenter.default.publisher(for: NSNotification.Name("CreatedNewWorkout"))
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let workouts = workouts {
|
||||
VStack {
|
||||
AllWorkoutPickerView(mainViews: MainViewTypes.allCases,
|
||||
selectedSegment: $selectedSegment,
|
||||
showCurrentWorkout: {
|
||||
selectedWorkout = bridgeModule.currentExerciseInfo.workout
|
||||
})
|
||||
|
||||
|
||||
switch selectedSegment {
|
||||
case .AllWorkout:
|
||||
if isUpdating {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
|
||||
AllWorkoutsListView(uniqueWorkoutUsers: $uniqueWorkoutUsers,
|
||||
workouts: workouts,
|
||||
selectedWorkout: { workout in
|
||||
selectedWorkout = workout
|
||||
}, refresh: {
|
||||
self.needsUpdating = true
|
||||
maybeUpdateShit()
|
||||
})
|
||||
Divider()
|
||||
case .MyWorkouts:
|
||||
plannedWorkout(workouts: UserStore.shared.plannedWorkouts)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ProgressView("Updating")
|
||||
}
|
||||
}.onAppear{
|
||||
// UserStore.shared.logout()
|
||||
authorizeHealthKit()
|
||||
maybeUpdateShit()
|
||||
}
|
||||
.sheet(item: $selectedWorkout) { item in
|
||||
var isPreview = item.id == bridgeModule.currentExerciseInfo.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
|
||||
maybeUpdateShit()
|
||||
})
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
.onReceive(pub) { (output) in
|
||||
self.needsUpdating = true
|
||||
maybeUpdateShit()
|
||||
}
|
||||
}
|
||||
|
||||
func plannedWorkout(workouts: [PlannedWorkout]) -> some View {
|
||||
List {
|
||||
ForEach(workouts, id:\.workout.name) { plannedWorkout in
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(plannedWorkout.onDate.plannedDate?.weekDay ?? "-")
|
||||
.font(.title)
|
||||
|
||||
Text(plannedWorkout.onDate.plannedDate?.monthString ?? "-")
|
||||
.font(.title)
|
||||
|
||||
Text(plannedWorkout.onDate.plannedDate?.dateString ?? "-")
|
||||
.font(.title)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack {
|
||||
Text(plannedWorkout.workout.name)
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Text(plannedWorkout.workout.description ?? "")
|
||||
.font(.body)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Text(plannedWorkout.onDate)
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedPlannedWorkout = plannedWorkout.workout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maybeUpdateShit() {
|
||||
if UserStore.shared.token != nil{
|
||||
if UserStore.shared.plannedWorkouts.isEmpty {
|
||||
UserStore.shared.fetchPlannedWorkouts()
|
||||
}
|
||||
|
||||
if needsUpdating {
|
||||
self.isUpdating = true
|
||||
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 {
|
||||
showLoginView = true
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeHealthKit() {
|
||||
let healthKitTypes: Set = [
|
||||
HKObjectType.quantityType(forIdentifier: .heartRate)!,
|
||||
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!,
|
||||
HKQuantityType.workoutType()
|
||||
]
|
||||
|
||||
healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { (succ, error) in
|
||||
if !succ {
|
||||
fatalError("Error requesting authorization from health store: \(String(describing: error)))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AllWorkoutsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AllWorkoutsView(workouts: PreviewData.allWorkouts())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// CompletedWorkoutView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/22/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import HealthKit
|
||||
|
||||
struct CompletedWorkoutView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
@State var healthKitWorkoutData: HealthKitWorkoutData?
|
||||
@State var difficulty: Float = 0
|
||||
@State var notes: String = ""
|
||||
@State var isUploading: Bool = false
|
||||
@State var gettingHealthKitData: Bool = false
|
||||
|
||||
var postData: [String: Any]
|
||||
let healthKitHelper = HealthKitHelper()
|
||||
let workout: Workout
|
||||
let completedWorkoutDismissed: ((Bool) -> Void)?
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if isUploading {
|
||||
ProgressView("Uploading")
|
||||
}
|
||||
VStack {
|
||||
topViews()
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
if let calsBurned = healthKitWorkoutData?.caloriesBurned {
|
||||
HStack {
|
||||
HStack {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.title)
|
||||
VStack {
|
||||
Text("\(calsBurned, specifier: "%.0f")")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
if let minHeart = healthKitWorkoutData?.minHeartRate,
|
||||
let maxHeart = healthKitWorkoutData?.maxHeartRate,
|
||||
let avgHeart = healthKitWorkoutData?.avgHeartRate {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "heart")
|
||||
.foregroundColor(.red)
|
||||
.font(.title)
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(minHeart, specifier: "%.0f")")
|
||||
Text("-")
|
||||
Text("\(maxHeart, specifier: "%.0f")")
|
||||
}
|
||||
Text("\(avgHeart, specifier: "%.0f")")
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateWorkout()
|
||||
.frame(maxHeight: 88)
|
||||
|
||||
Divider()
|
||||
|
||||
TextField("Notes", text: $notes)
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.padding([.horizontal], 4)
|
||||
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 200/255, green: 200/255, blue: 200/255, alpha: 0.2)))
|
||||
.cornerRadius(8)
|
||||
|
||||
if gettingHealthKitData {
|
||||
ProgressView("Getting HealthKit data")
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Upload", action: {
|
||||
isUploading = true
|
||||
upload(postBody: postData)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
}
|
||||
.onAppear{
|
||||
bridgeModule.sendWorkoutCompleteToWatch()
|
||||
}
|
||||
.onChange(of: bridgeModule.healthKitUUID, perform: { healthKitUUID in
|
||||
if let healthKitUUID = healthKitUUID {
|
||||
gettingHealthKitData = true
|
||||
healthKitHelper.getDetails(forHealthKitUUID: healthKitUUID,
|
||||
completion: { healthKitWorkoutData in
|
||||
self.healthKitWorkoutData = healthKitWorkoutData
|
||||
gettingHealthKitData = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func topViews() -> some View {
|
||||
VStack {
|
||||
Text(workout.name)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.title3)
|
||||
.padding(.top
|
||||
)
|
||||
if let desc = workout.description {
|
||||
Text(desc)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.body)
|
||||
.padding(.top)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rateWorkout() -> some View {
|
||||
VStack {
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Text("No Rate")
|
||||
.foregroundColor(.black)
|
||||
Text("Easy")
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
Text("Death")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [.black, .green, .red]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.mask(Slider(value: $difficulty, in: 0...5, step: 1))
|
||||
|
||||
// Dummy replicated slider, to allow sliding
|
||||
Slider(value: $difficulty, in: 0...5, step: 1)
|
||||
.opacity(0.05) // Opacity is the trick here.
|
||||
.accentColor(.clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func upload(postBody: [String: Any]) {
|
||||
var _postBody = postBody
|
||||
_postBody["difficulty"] = difficulty
|
||||
_postBody["notes"] = notes
|
||||
if let healthKitUUID = bridgeModule.healthKitUUID {
|
||||
_postBody["health_kit_workout_uuid"] = healthKitUUID.uuidString
|
||||
}
|
||||
|
||||
CompleteWorkoutFetchable(postData: _postBody).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
DispatchQueue.main.async {
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
dismiss()
|
||||
completedWorkoutDismissed?(true)
|
||||
}
|
||||
case .failure(let failure):
|
||||
DispatchQueue.main.async {
|
||||
self.isUploading = false
|
||||
}
|
||||
print(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletedWorkoutView_Previews: PreviewProvider {
|
||||
static let postBody = [
|
||||
"difficulty": 1,
|
||||
"workout_start_time": Date().timeFormatForUpload,
|
||||
"workout": 1,
|
||||
"total_time": 140,
|
||||
"total_calories": Float(120.0),
|
||||
"heart_rates": [65,65,4,54,232,12]
|
||||
] as [String : Any]
|
||||
|
||||
static let workout = PreviewData.workout()
|
||||
|
||||
static var previews: some View {
|
||||
CompletedWorkoutView(postData: CompletedWorkoutView_Previews.postBody,
|
||||
workout: workout,
|
||||
completedWorkoutDismissed: { _ in })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// CreateViewRepsWeightView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateExerciseActionsView: View {
|
||||
@ObservedObject var workoutExercise: CreateWorkoutExercise
|
||||
var superset: CreateWorkoutSuperSet
|
||||
var viewModel: WorkoutViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
VStack {
|
||||
Text("Reps: ")
|
||||
Text("\(workoutExercise.reps)")
|
||||
.foregroundColor(workoutExercise.reps == 0 && workoutExercise.duration == 0 ? .red : Color(uiColor: .label))
|
||||
.bold()
|
||||
}
|
||||
Stepper("", onIncrement: {
|
||||
workoutExercise.increaseReps()
|
||||
}, onDecrement: {
|
||||
workoutExercise.decreaseReps()
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Divider()
|
||||
|
||||
VStack{
|
||||
VStack {
|
||||
Text("Weight: ")
|
||||
Text("\(workoutExercise.weight)")
|
||||
}
|
||||
Stepper("", onIncrement: {
|
||||
workoutExercise.increaseWeight()
|
||||
}, onDecrement: {
|
||||
workoutExercise.decreaseWeight()
|
||||
})
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Divider()
|
||||
|
||||
VStack{
|
||||
VStack {
|
||||
Text("Duration: ")
|
||||
Text("\(workoutExercise.duration)")
|
||||
.foregroundColor(workoutExercise.reps == 0 && workoutExercise.duration == 0 ? .red : Color(uiColor: .label))
|
||||
.bold()
|
||||
}
|
||||
Stepper("", onIncrement: {
|
||||
workoutExercise.increaseDuration()
|
||||
}, onDecrement: {
|
||||
workoutExercise.decreaseDuration()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
}) {
|
||||
Image(systemName: "video.fill")
|
||||
}
|
||||
.frame(width: 88, height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.blue)
|
||||
.cornerRadius(10)
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Divider()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
superset.deleteExerciseForChosenSuperset(exercise: workoutExercise)
|
||||
viewModel.increaseRandomNumberForUpdating()
|
||||
viewModel.objectWillChange.send()
|
||||
}) {
|
||||
Image(systemName: "trash.fill")
|
||||
}
|
||||
.frame(width: 88, height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.red)
|
||||
.cornerRadius(10)
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
.background(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct CreateViewRepsWeightView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// CreateViewRepsWeightView()
|
||||
// }
|
||||
//}
|
||||
170
iphone/Werkout_ios/Views/CreateWorkout/CreateViewModels.swift
Normal file
170
iphone/Werkout_ios/Views/CreateWorkout/CreateViewModels.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// CreateViewModels.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class CreateWorkoutExercise: ObservableObject, Identifiable {
|
||||
let id = UUID()
|
||||
var exercise: Exercise
|
||||
@Published var reps: Int = 0
|
||||
@Published var duration: Int = 0
|
||||
@Published var weight: Int = 0
|
||||
|
||||
init(exercise: Exercise, reps: Int = 0, duration: Int = 0, weight: Int = 0) {
|
||||
self.exercise = exercise
|
||||
self.reps = reps
|
||||
self.duration = duration
|
||||
self.weight = weight
|
||||
}
|
||||
|
||||
func increaseReps() {
|
||||
self.reps += 1
|
||||
}
|
||||
|
||||
func decreaseReps() {
|
||||
self.reps -= 1
|
||||
if self.reps < 0 {
|
||||
self.reps = 0
|
||||
}
|
||||
}
|
||||
|
||||
func increaseDuration() {
|
||||
self.duration += 15
|
||||
self.reps = 0
|
||||
}
|
||||
|
||||
func decreaseDuration() {
|
||||
self.duration -= 15
|
||||
if self.duration < 0 {
|
||||
self.duration = 0
|
||||
}
|
||||
}
|
||||
|
||||
func increaseWeight() {
|
||||
self.weight += 5
|
||||
}
|
||||
|
||||
func decreaseWeight() {
|
||||
self.weight -= 15
|
||||
if self.weight < 0 {
|
||||
self.weight = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateWorkoutSuperSet: ObservableObject, Identifiable, Equatable {
|
||||
static func == (lhs: CreateWorkoutSuperSet, rhs: CreateWorkoutSuperSet) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
@Published var exercises = [CreateWorkoutExercise]()
|
||||
@Published var numberOfRounds = 0
|
||||
|
||||
func increaseNumberOfRounds() {
|
||||
self.numberOfRounds += 1
|
||||
}
|
||||
|
||||
func decreaseNumberOfRounds() {
|
||||
self.numberOfRounds -= 1
|
||||
if self.numberOfRounds < 0 {
|
||||
self.numberOfRounds = 0
|
||||
}
|
||||
}
|
||||
|
||||
func deleteExerciseForChosenSuperset(exercise: CreateWorkoutExercise) {
|
||||
if let idx = exercises.firstIndex(where: {
|
||||
$0.id == exercise.id
|
||||
}) {
|
||||
exercises.remove(at: idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WorkoutViewModel: ObservableObject {
|
||||
@Published var superSets = [CreateWorkoutSuperSet]()
|
||||
@Published var title = String()
|
||||
@Published var description = String()
|
||||
@Published var randomValueForUpdatingValue = 0
|
||||
|
||||
func increaseRandomNumberForUpdating() {
|
||||
randomValueForUpdatingValue += 1
|
||||
}
|
||||
|
||||
func addNewSuperset() {
|
||||
increaseRandomNumberForUpdating()
|
||||
superSets.append(CreateWorkoutSuperSet())
|
||||
}
|
||||
|
||||
func delete(superset: CreateWorkoutSuperSet) {
|
||||
if let idx = superSets.firstIndex(where: {
|
||||
$0.id == superset.id
|
||||
}) {
|
||||
superSets.remove(at: idx)
|
||||
increaseRandomNumberForUpdating()
|
||||
}
|
||||
}
|
||||
|
||||
func showRoundsError() {
|
||||
|
||||
}
|
||||
|
||||
func showNoDurationOrReps() {
|
||||
|
||||
}
|
||||
|
||||
func uploadWorkout() {
|
||||
var supersets = [[String: Any]]()
|
||||
var supersetOrder = 1
|
||||
superSets.forEach({ superset in
|
||||
if superset.numberOfRounds == 0 {
|
||||
showRoundsError()
|
||||
return
|
||||
}
|
||||
var supersetInfo = [String: Any]()
|
||||
supersetInfo["name"] = ""
|
||||
supersetInfo["rounds"] = superset.numberOfRounds
|
||||
supersetInfo["order"] = supersetOrder
|
||||
|
||||
var exercises = [[String: Any]]()
|
||||
var exerciseOrder = 1
|
||||
for exercise in superset.exercises {
|
||||
if exercise.reps == 0 && exercise.duration == 0 {
|
||||
showNoDurationOrReps()
|
||||
return
|
||||
}
|
||||
|
||||
let item = ["id": exercise.exercise.id,
|
||||
"reps": exercise.reps,
|
||||
"weight": exercise.weight,
|
||||
"duration": exercise.duration,
|
||||
"order": exerciseOrder] as [String : Any]
|
||||
exercises.append(item)
|
||||
exerciseOrder += 1
|
||||
}
|
||||
supersetInfo["exercises"] = exercises
|
||||
|
||||
supersets.append(supersetInfo)
|
||||
supersetOrder += 1
|
||||
})
|
||||
let uploadBody = ["name": title,
|
||||
"description": description,
|
||||
"supersets": supersets] as [String : Any]
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// CreateWorkoutItemPicker.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/25/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct CreateWorkoutItemPickerModel {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
class CreateWorkoutItemPickerViewModel: Identifiable, ObservableObject {
|
||||
let allValues: [CreateWorkoutItemPickerModel]
|
||||
@Published var selectedIds: [Int]
|
||||
|
||||
init(allValues: [CreateWorkoutItemPickerModel], selectedIds: [Int]) {
|
||||
self.allValues = allValues
|
||||
self.selectedIds = selectedIds
|
||||
}
|
||||
|
||||
func toggleAll() {
|
||||
if selectedIds.isEmpty {
|
||||
selectedIds.append(contentsOf: allValues.map({ $0.id }))
|
||||
} else {
|
||||
selectedIds.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateWorkoutItemPickerView: View {
|
||||
@ObservedObject var viewModel: CreateWorkoutItemPickerViewModel
|
||||
var completed: (([Int]) -> Void)
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var searchString: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List() {
|
||||
ForEach(viewModel.allValues, id:\.self.id) { value in
|
||||
if searchString.isEmpty || value.name.lowercased().contains(searchString.lowercased()) {
|
||||
HStack {
|
||||
Circle()
|
||||
.stroke(.blue, lineWidth: 1)
|
||||
.background(Circle().fill(viewModel.selectedIds.contains(value.id) ? .blue :.clear))
|
||||
.frame(width: 33, height: 33)
|
||||
|
||||
Text(value.name)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if viewModel.selectedIds.contains(value.id) {
|
||||
if let idx = viewModel.selectedIds.firstIndex(of: value.id){
|
||||
viewModel.selectedIds.remove(at: idx)
|
||||
}
|
||||
} else {
|
||||
viewModel.selectedIds.append(value.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField("Filter", text: $searchString)
|
||||
.padding()
|
||||
|
||||
|
||||
HStack {
|
||||
Button(action: {
|
||||
viewModel.toggleAll()
|
||||
}, label: {
|
||||
Image(systemName: "checklist")
|
||||
.font(.title)
|
||||
})
|
||||
.frame(maxWidth: 44, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.green)
|
||||
.background(.white)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
completed(viewModel.selectedIds)
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("done")
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateWorkoutItemPickerView_Previews: PreviewProvider {
|
||||
static let fakeValues = [CreateWorkoutItemPickerModel(id: 1, name: "one"),
|
||||
CreateWorkoutItemPickerModel(id: 2, name: "two"),
|
||||
CreateWorkoutItemPickerModel(id: 3, name: "three")]
|
||||
|
||||
static var previews: some View {
|
||||
CreateWorkoutItemPickerView(viewModel: CreateWorkoutItemPickerViewModel(allValues: fakeValues, selectedIds: [1]), completed: { selectedIds in
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// CreateWorkoutView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/15/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateWorkoutMainView: View {
|
||||
@ObservedObject var viewModel = WorkoutViewModel()
|
||||
@State private var showAddExercise = false
|
||||
@State var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
TextField("Title", text: $viewModel.title)
|
||||
.padding()
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(OvalTextFieldStyle())
|
||||
|
||||
TextField("Description", text: $viewModel.description)
|
||||
.padding()
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(OvalTextFieldStyle())
|
||||
ScrollViewReader { proxy in
|
||||
List() {
|
||||
ForEach($viewModel.superSets, id: \.id) { superset in
|
||||
Section {
|
||||
ForEach(superset.exercises, id: \.id) { exercise in
|
||||
HStack {
|
||||
VStack {
|
||||
Text(exercise.wrappedValue.exercise.name)
|
||||
.font(.title2)
|
||||
.frame(maxWidth: .infinity)
|
||||
if exercise.wrappedValue.exercise.side != nil && exercise.wrappedValue.exercise.side!.count > 0 {
|
||||
Text(exercise.wrappedValue.exercise.side!)
|
||||
.font(.title3)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
CreateExerciseActionsView(workoutExercise: exercise.wrappedValue,
|
||||
superset: superset.wrappedValue,
|
||||
viewModel: viewModel)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Stepper("Number of rounds", onIncrement: {
|
||||
superset.wrappedValue.increaseNumberOfRounds()
|
||||
viewModel.increaseRandomNumberForUpdating()
|
||||
viewModel.objectWillChange.send()
|
||||
}, onDecrement: {
|
||||
superset.wrappedValue.decreaseNumberOfRounds()
|
||||
viewModel.increaseRandomNumberForUpdating()
|
||||
viewModel.objectWillChange.send()
|
||||
})
|
||||
|
||||
Text("\(superset.wrappedValue.numberOfRounds)")
|
||||
.foregroundColor(superset.numberOfRounds.wrappedValue > 0 ? .black : .red)
|
||||
.bold()
|
||||
}
|
||||
|
||||
CreateWorkoutSupersetActionsView(workoutSuperSet: superset.wrappedValue,
|
||||
showAddExercise: $showAddExercise,
|
||||
viewModel: viewModel,
|
||||
selectedCreateWorkoutSuperSet: $selectedCreateWorkoutSuperSet)
|
||||
}
|
||||
}
|
||||
Text("this is the bottom 🤷♂️")
|
||||
.id(999)
|
||||
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
.onChange(of: viewModel.randomValueForUpdatingValue, perform: { newValue in
|
||||
withAnimation {
|
||||
proxy.scrollTo(999, anchor: .bottom)
|
||||
}
|
||||
})
|
||||
}
|
||||
// .overlay(Group {
|
||||
// if($viewModel.superSets.isEmpty) {
|
||||
// ZStack() {
|
||||
// Color(uiColor: .secondarySystemBackground)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Button("Add Superset", action: {
|
||||
viewModel.addNewSuperset()
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Done", action: {
|
||||
viewModel.uploadWorkout()
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.blue)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(viewModel.title.isEmpty)
|
||||
}
|
||||
.frame(height: 44)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.sheet(isPresented: $showAddExercise) {
|
||||
AddExerciseView(selectedExercise: { exercise in
|
||||
let workoutExercise = CreateWorkoutExercise(exercise: exercise)
|
||||
selectedCreateWorkoutSuperSet?.exercises.append(workoutExercise)
|
||||
|
||||
|
||||
// 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 {
|
||||
let exercises = DataStore.shared.allExercise?.filter({
|
||||
$0.name == exercise.name
|
||||
})
|
||||
let recover = DataStore.shared.allExercise?.first(where: {
|
||||
$0.name.lowercased() == "recover"
|
||||
})
|
||||
if let exercises = exercises, let recover = recover {
|
||||
if exercises.count == 2 {
|
||||
let recoverWorkoutExercise = CreateWorkoutExercise(exercise: recover)
|
||||
selectedCreateWorkoutSuperSet?.exercises.append(recoverWorkoutExercise)
|
||||
for LRExercise in exercises {
|
||||
if LRExercise.id != exercise.id {
|
||||
let otherSideExercise = CreateWorkoutExercise(exercise: LRExercise)
|
||||
selectedCreateWorkoutSuperSet?.exercises.append(otherSideExercise)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.increaseRandomNumberForUpdating()
|
||||
viewModel.objectWillChange.send()
|
||||
selectedCreateWorkoutSuperSet = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct CreateWorkoutView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// CreateWorkoutView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// CreateWorkoutSupersetActionsView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateWorkoutSupersetActionsView: View {
|
||||
var workoutSuperSet: CreateWorkoutSuperSet
|
||||
@Binding var showAddExercise: Bool
|
||||
var viewModel: WorkoutViewModel
|
||||
@Binding var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
selectedCreateWorkoutSuperSet = workoutSuperSet
|
||||
showAddExercise.toggle()
|
||||
}) {
|
||||
Text("Add exercise")
|
||||
.padding()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.background(.green)
|
||||
.cornerRadius(10)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Button(action: {
|
||||
viewModel.delete(superset: workoutSuperSet)
|
||||
viewModel.increaseRandomNumberForUpdating()
|
||||
viewModel.objectWillChange.send()
|
||||
|
||||
}) {
|
||||
Text("Delete superset")
|
||||
.padding()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.background(.red)
|
||||
.cornerRadius(10)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct CreateWorkoutSupersetActionsView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// CreateWorkoutSupersetActionsView()
|
||||
// }
|
||||
//}
|
||||
116
iphone/Werkout_ios/Views/ExternalWorkoutDetailView.swift
Normal file
116
iphone/Werkout_ios/Views/ExternalWorkoutDetailView.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// ExternalView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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")!)
|
||||
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
|
||||
@AppStorage(Constants.extShowNextVideo) private var extShowNextVideo: Bool = false
|
||||
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let workout = bridgeModule.currentExerciseInfo.workout {
|
||||
GeometryReader { metrics in
|
||||
VStack {
|
||||
HStack {
|
||||
if extThotStyle != .off {
|
||||
PlayerView(player: $avPlayer)
|
||||
.frame(width: metrics.size.width * 0.5, height: metrics.size.height * 0.8)
|
||||
.onAppear{
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
ExtExerciseList(workout: workout,
|
||||
allSupersetExecerciseIndex: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex)
|
||||
}
|
||||
.frame(width: metrics.size.width * 0.4, height: metrics.size.height * 0.8)
|
||||
.padding([.top, .bottom], 20)
|
||||
}
|
||||
|
||||
HStack {
|
||||
ExtCountdownView()
|
||||
.padding(.leading, 50)
|
||||
.padding(.bottom, 20)
|
||||
if extShowNextVideo && extThotStyle != .off {
|
||||
PlayerView(player: $smallAVPlayer)
|
||||
.frame(width: metrics.size.width * 0.2,
|
||||
height: metrics.size.height * 0.2)
|
||||
.onAppear{
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image("icon")
|
||||
.resizable()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.scaledToFill()
|
||||
}
|
||||
}
|
||||
.onChange(of: bridgeModule.isInWorkout, perform: { _ in
|
||||
playVideos()
|
||||
})
|
||||
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { _ in
|
||||
playVideos()
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(bridgeModule.currentExerciseInfo.workout == nil ? Color(red: 157/255, green: 138/255, blue: 255/255) : Color(uiColor: .systemBackground))
|
||||
.onReceive(NotificationCenter.default.publisher(
|
||||
for: UIScene.willEnterForegroundNotification)) { _ in
|
||||
avPlayer.play()
|
||||
smallAVPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
func playVideos() {
|
||||
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
|
||||
if let videoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: extThotStyle,
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: currentExtercise.exercise.videoURL,
|
||||
exerciseName: currentExtercise.exercise.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout) {
|
||||
avPlayer = AVPlayer(url: videoURL)
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
if let smallVideoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: .never,
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: BridgeModule.shared.currentExerciseInfo.nextExerciseInfo?.exercise.videoURL,
|
||||
exerciseName: BridgeModule.shared.currentExerciseInfo.nextExerciseInfo?.exercise.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout),
|
||||
extShowNextVideo {
|
||||
smallAVPlayer = AVPlayer(url: smallVideoURL)
|
||||
smallAVPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct ExternalWorkoutDetailView_Previews: PreviewProvider {
|
||||
// static var bridge = BridgeModule.shared
|
||||
//
|
||||
// static var previews: some View {
|
||||
// ExternalWorkoutDetailView().environmentObject({ () -> BridgeModule in
|
||||
// let envObj = BridgeModule.shared
|
||||
// envObj.currentWorkout = nil //PreviewData.workout()
|
||||
// bridge.currentExercise = PreviewData.workout().exercisesSortedByCreated_at.first!
|
||||
// return envObj
|
||||
// }() )
|
||||
// }
|
||||
//}
|
||||
93
iphone/Werkout_ios/Views/Login/LoginView.swift
Normal file
93
iphone/Werkout_ios/Views/Login/LoginView.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// LoginView.swift
|
||||
// WekoutThotViewer
|
||||
//
|
||||
// Created by Trey Tartt on 6/18/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoginView: View {
|
||||
@State var email: String = ""
|
||||
@State var password: String = ""
|
||||
@Environment(\.dismiss) var dismiss
|
||||
let completion: (() -> Void)
|
||||
@State var doingNetworkShit: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField("Email", text: $email)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.padding([.horizontal], 4)
|
||||
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 255/255, green: 255/255, blue: 255/255, alpha: 1)))
|
||||
.cornerRadius(8)
|
||||
.padding(.top, 25)
|
||||
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.password)
|
||||
.autocapitalization(.none)
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.padding([.horizontal], 4)
|
||||
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 255/255, green: 255/255, blue: 255/255, alpha: 1)))
|
||||
.cornerRadius(8)
|
||||
|
||||
if doingNetworkShit {
|
||||
ProgressView("Logging In")
|
||||
.padding()
|
||||
.foregroundColor(.white)
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.5, anchor: .center)
|
||||
} else {
|
||||
Button("Login", action: {
|
||||
login()
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(password.isEmpty || email.isEmpty)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
Image("icon")
|
||||
.resizable()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.scaledToFill()
|
||||
)
|
||||
}
|
||||
|
||||
func login() {
|
||||
let postData = [
|
||||
"email": email,
|
||||
"password": password
|
||||
]
|
||||
doingNetworkShit = true
|
||||
UserStore.shared.login(postData: postData, completion: { success in
|
||||
doingNetworkShit = false
|
||||
if success {
|
||||
completion()
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoginView(completion: {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
31
iphone/Werkout_ios/Views/MainView.swift
Normal file
31
iphone/Werkout_ios/Views/MainView.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct MainView: View {
|
||||
@State var workout: Workout?
|
||||
@StateObject var bridgeModule = BridgeModule.shared
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let workout = workout {
|
||||
let vm = WorkoutDetailViewModel(workout: workout, isPreview: true)
|
||||
WorkoutDetailView(viewModel: vm)
|
||||
} else {
|
||||
Text("no workout selected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
94
iphone/Werkout_ios/Views/PlanWorkoutView.swift
Normal file
94
iphone/Werkout_ios/Views/PlanWorkoutView.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// PlanWorkoutView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/2/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PlanWorkoutView: View {
|
||||
@State var selectedDate = Date()
|
||||
let workout: Workout
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var addedPlannedWorkout: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
Text(workout.name)
|
||||
.font(.title)
|
||||
|
||||
Text(selectedDate.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.system(size: 28))
|
||||
.bold()
|
||||
.foregroundColor(Color.accentColor)
|
||||
.padding()
|
||||
.animation(.spring(), value: selectedDate)
|
||||
|
||||
Divider().frame(height: 1)
|
||||
|
||||
DatePicker("Select Date", selection: $selectedDate, displayedComponents: [.date])
|
||||
.padding(.horizontal)
|
||||
.datePickerStyle(.graphical)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Button(action: {
|
||||
planWorkout()
|
||||
}, label: {
|
||||
Image(systemName: "plus.app")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark.octagon.fill")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.red)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func planWorkout() {
|
||||
let postData = [
|
||||
"on_date": selectedDate.formatForPlannedWorkout,
|
||||
"workout": workout.id
|
||||
] as [String : Any]
|
||||
|
||||
PlanWorkoutFetchable(postData: postData).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
UserStore.shared.fetchPlannedWorkouts()
|
||||
dismiss()
|
||||
addedPlannedWorkout?()
|
||||
case .failure(_):
|
||||
fatalError("shit broke")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PlanWorkoutView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlanWorkoutView(workout: PreviewData.workout())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// CurrentWorkoutElapsedTimeView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CurrentWorkoutElapsedTimeView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
|
||||
var body: some View {
|
||||
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
|
||||
VStack {
|
||||
Text("\(Double(bridgeModule.currentWorkoutRunTimeInSeconds).asString(style: .positional))")
|
||||
.font(.title2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
iphone/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift
Normal file
160
iphone/Werkout_ios/Views/WorkoutDetail/ExerciseListView.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// ExerciseListView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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")!)
|
||||
var workout: Workout
|
||||
@Binding var showExecersizeInfo: Bool
|
||||
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
|
||||
|
||||
@State var videoExercise: Exercise? {
|
||||
didSet {
|
||||
if let videoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: phoneThotStyle,
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: self.videoExercise?.videoURL,
|
||||
exerciseName: self.videoExercise?.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout) {
|
||||
avPlayer = AVPlayer(url: videoURL)
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
VStack {
|
||||
HStack {
|
||||
if bridgeModule.isInWorkout &&
|
||||
supersetIndex == bridgeModule.currentExerciseInfo.supersetIndex &&
|
||||
exerciseIndex == bridgeModule.currentExerciseInfo.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 {
|
||||
if bridgeModule.isInWorkout {
|
||||
bridgeModule.goToExerciseAt(section: supersetIndex, row: exerciseIndex)
|
||||
} else {
|
||||
videoExercise = supersetExecercise.exercise
|
||||
}
|
||||
}
|
||||
|
||||
if bridgeModule.isInWorkout &&
|
||||
supersetIndex == bridgeModule.currentExerciseInfo.supersetIndex &&
|
||||
exerciseIndex == bridgeModule.currentExerciseInfo.exerciseIndex &&
|
||||
showExecersizeInfo {
|
||||
detailView(forExercise: supersetExecercise)
|
||||
}
|
||||
}.id(supersetExecercise.id)
|
||||
}
|
||||
}, header: {
|
||||
HStack {
|
||||
Text(superset.name ?? "--")
|
||||
.foregroundColor(Color("appColor"))
|
||||
.bold()
|
||||
Spacer()
|
||||
Text("\(superset.rounds) rounds")
|
||||
.foregroundColor(Color("appColor"))
|
||||
.bold()
|
||||
|
||||
if let estimatedTime = superset.estimatedTime {
|
||||
Text("@ " + estimatedTime.asString(style: .abbreviated))
|
||||
.foregroundColor(Color("appColor"))
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { newValue in
|
||||
if let newCurrentExercise = bridgeModule.currentExerciseInfo.currentExercise {
|
||||
withAnimation {
|
||||
proxy.scrollTo(newCurrentExercise.id, anchor: .top)
|
||||
}
|
||||
}
|
||||
})
|
||||
.sheet(item: $videoExercise) { exercise in
|
||||
PlayerView(player: $avPlayer)
|
||||
.onAppear{
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func detailView(forExercise supersetExecercise: SupersetExercise) -> some View {
|
||||
VStack {
|
||||
Text(supersetExecercise.exercise.description)
|
||||
.frame(alignment: .leading)
|
||||
Divider()
|
||||
Text(supersetExecercise.exercise.muscles.map({ $0.name }).joined(separator: ", "))
|
||||
.frame(alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct ExerciseListView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ExerciseListView(workout: PreviewData.workout(), showExecersizeInfo: )
|
||||
// }
|
||||
//}
|
||||
229
iphone/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift
Normal file
229
iphone/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift
Normal file
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// MainView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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")!)
|
||||
|
||||
@StateObject 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 UUID().uuidString }
|
||||
}
|
||||
|
||||
@State var presentedSheet: Sheet?
|
||||
@State var workoutToPlan: Workout?
|
||||
@State var showExecersizeInfo: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch viewModel.status {
|
||||
case .loading:
|
||||
Text("Loading")
|
||||
case .showWorkout(let workout):
|
||||
VStack(spacing: 0) {
|
||||
if bridgeModule.isInWorkout {
|
||||
HStack {
|
||||
CountdownView()
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
if phoneThotStyle != .off {
|
||||
GeometryReader { metrics in
|
||||
ZStack {
|
||||
PlayerView(player: $avPlayer)
|
||||
.frame(width: metrics.size.width * 1, height: metrics.size.height * 1)
|
||||
.onAppear{
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if let assetURL = ((avPlayer.currentItem?.asset) as? AVURLAsset)?.url,
|
||||
let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise,
|
||||
let otherVideoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: VideoURLCreator.otherVideoType(forVideoURL: assetURL),
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: currentExtercise.exercise.videoURL,
|
||||
exerciseName: currentExtercise.exercise.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout) {
|
||||
avPlayer = AVPlayer(url: otherVideoURL)
|
||||
avPlayer.play()
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(Color("appColor"))
|
||||
})
|
||||
.foregroundColor(.blue)
|
||||
.cornerRadius(4)
|
||||
.frame(width: 160, height: 120)
|
||||
.position(x: metrics.size.width - 22, y: metrics.size.height - 30)
|
||||
|
||||
Button(action: {
|
||||
showExecersizeInfo.toggle()
|
||||
}, label: {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(Color("appColor"))
|
||||
})
|
||||
.foregroundColor(.blue)
|
||||
.cornerRadius(4)
|
||||
.frame(width: 120, height: 120)
|
||||
.position(x: 22, y: metrics.size.height - 30)
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
.background(Color(uiColor: .tertiarySystemBackground))
|
||||
}
|
||||
}
|
||||
|
||||
if !bridgeModule.isInWorkout {
|
||||
InfoView(workout: workout)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
if bridgeModule.isInWorkout {
|
||||
Divider()
|
||||
.background(Color(uiColor: .secondaryLabel))
|
||||
HStack {
|
||||
Text("\(bridgeModule.currentExerciseInfo.currentRound) of \(bridgeModule.currentExerciseInfo.numberOfRoundsInCurrentSuperSet)")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 10)
|
||||
|
||||
CurrentWorkoutElapsedTimeView()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text("\(bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex+1)/\(bridgeModule.currentExerciseInfo.workout?.allSupersetExecercise?.count ?? -99)")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
.background(Color(uiColor: .tertiarySystemBackground))
|
||||
}
|
||||
|
||||
Divider()
|
||||
.background(Color(uiColor: .secondaryLabel))
|
||||
|
||||
ExerciseListView(workout: workout, showExecersizeInfo: $showExecersizeInfo)
|
||||
.padding([.top, .bottom], 10)
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
|
||||
ActionsView(completedWorkout: {
|
||||
bridgeModule.completeWorkout()
|
||||
}, planWorkout: { workout in
|
||||
workoutToPlan = workout
|
||||
}, workout: workout, showAddToCalendar: viewModel.isPreview, startWorkoutAction: {
|
||||
startWorkout(workout: workout)
|
||||
})
|
||||
.frame(height: 44)
|
||||
|
||||
}
|
||||
.sheet(item: $presentedSheet) { item in
|
||||
switch item {
|
||||
case .completedWorkout(let data):
|
||||
CompletedWorkoutView(postData: data,
|
||||
workout: workout,
|
||||
completedWorkoutDismissed: { uploaded in
|
||||
if uploaded {
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.sheet(item: $workoutToPlan) { workout in
|
||||
PlanWorkoutView(workout: workout, addedPlannedWorkout: {
|
||||
dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { _ in
|
||||
playVideos()
|
||||
})
|
||||
.onChange(of: bridgeModule.isInWorkout, perform: { _ in
|
||||
playVideos()
|
||||
})
|
||||
.onAppear{
|
||||
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
|
||||
if let videoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: phoneThotStyle,
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: currentExtercise.exercise.videoURL,
|
||||
exerciseName: currentExtercise.exercise.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout) {
|
||||
avPlayer = AVPlayer(url: videoURL)
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(
|
||||
for: UIScene.willEnterForegroundNotification)) { _ in
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
func playVideos() {
|
||||
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
|
||||
if let videoURL = VideoURLCreator.videoURL(
|
||||
thotStyle: phoneThotStyle,
|
||||
gender: thotGenderOption,
|
||||
defaultVideoURLStr: currentExtercise.exercise.videoURL,
|
||||
exerciseName: currentExtercise.exercise.name,
|
||||
workout: bridgeModule.currentExerciseInfo.workout) {
|
||||
avPlayer = AVPlayer(url: videoURL)
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startWorkout(workout: Workout) {
|
||||
bridgeModule.completedWorkout = {
|
||||
if let workoutData = createWorkoutData() {
|
||||
presentedSheet = .completedWorkout(workoutData)
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
bridgeModule.start(workout: workout)
|
||||
}
|
||||
|
||||
func createWorkoutData() -> [String:Any]? {
|
||||
guard let workoutid = bridgeModule.currentExerciseInfo.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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// MainViewViewModel.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/14/23.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class WorkoutDetailViewModel: ObservableObject {
|
||||
enum WorkoutDetailViewModelStatus {
|
||||
case loading
|
||||
case showWorkout(Workout)
|
||||
}
|
||||
|
||||
@Published var status: WorkoutDetailViewModelStatus
|
||||
let isPreview: Bool
|
||||
|
||||
init(workout: Workout, status: WorkoutDetailViewModelStatus? = nil, isPreview: Bool) {
|
||||
self.status = .loading
|
||||
self.isPreview = isPreview
|
||||
|
||||
if let passedStatus = status {
|
||||
self.status = passedStatus
|
||||
} else {
|
||||
WorkoutDetailFetchable(workoutID: workout.id).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
DispatchQueue.main.async {
|
||||
self.status = .showWorkout(model)
|
||||
}
|
||||
case .failure(let failure):
|
||||
fatalError("failed \(failure.localizedDescription)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
93
iphone/Werkout_ios/Views/WorkoutHistoryView.swift
Normal file
93
iphone/Werkout_ios/Views/WorkoutHistoryView.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// WorkoutHistoryView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/26/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WorkoutHistoryView: View {
|
||||
enum DifficltyString: Int {
|
||||
case easy = 1
|
||||
case moderate
|
||||
case average
|
||||
case hard
|
||||
case death
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
|
||||
case .easy:
|
||||
return "Easy"
|
||||
case .moderate:
|
||||
return "Moderate"
|
||||
case .average:
|
||||
return "Average"
|
||||
case .hard:
|
||||
return "Hard"
|
||||
case .death:
|
||||
return "Death"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let completedWorkouts: [CompletedWorkout]
|
||||
|
||||
@State private var selectedPlannedWorkout: Workout?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(completedWorkouts, id:\.self.id) { completedWorkout in
|
||||
HStack {
|
||||
VStack {
|
||||
if let date = completedWorkout.workoutStartTime.dateFromServerDate {
|
||||
Text(DateFormatter().shortMonthSymbols[date.get(.month) - 1])
|
||||
|
||||
Text("\(date.get(.day))")
|
||||
Text("\(date.get(.hour))")
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(completedWorkout.workout.name)
|
||||
.font(.title3)
|
||||
|
||||
if let desc = completedWorkout.workout.description {
|
||||
Text(desc)
|
||||
.font(.footnote)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
if let difficulty = completedWorkout.difficulty,
|
||||
let string = DifficltyString.init(rawValue: difficulty)?.stringValue {
|
||||
Text(string)
|
||||
}
|
||||
|
||||
if let notes = completedWorkout.notes {
|
||||
Text(notes)
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedPlannedWorkout = completedWorkout.workout
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedPlannedWorkout) { item in
|
||||
let viewModel = WorkoutDetailViewModel(workout: item, isPreview: true)
|
||||
WorkoutDetailView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkoutHistoryView_Previews: PreviewProvider {
|
||||
static let fakeHistory = PreviewData.parseCompletedWorkouts()
|
||||
static var previews: some View {
|
||||
WorkoutHistoryView(completedWorkouts: fakeHistory)
|
||||
}
|
||||
}
|
||||
19
iphone/Werkout_ios/WatchPackageModel.swift
Normal file
19
iphone/Werkout_ios/WatchPackageModel.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// WatchPackageModel.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/22/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct WatchPackageModel: Codable {
|
||||
var currentExerciseName: String
|
||||
var currentTimeLeft: Int
|
||||
var workoutStartDate: Date
|
||||
var workoutEndDate: Date?
|
||||
}
|
||||
|
||||
struct WatchFinishWorkoutModel: Codable {
|
||||
var healthKitUUID: UUID
|
||||
}
|
||||
20
iphone/Werkout_ios/Werkout_ios.entitlements
Normal file
20
iphone/Werkout_ios/Werkout_ios.entitlements
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>webcredentials:dev.werkout.fitness</string>
|
||||
</array>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.healthkit.background-delivery</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Werkout_ios.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
</elements>
|
||||
</model>
|
||||
105
iphone/Werkout_ios/Werkout_iosApp.swift
Normal file
105
iphone/Werkout_ios/Werkout_iosApp.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Werkout_iosApp.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import AVKit
|
||||
|
||||
|
||||
struct Constants {
|
||||
static let phoneThotStyle = "phoneThotStyle"
|
||||
static let extThotStyle = "extThotStyle"
|
||||
static let extShowNextVideo = "extShowNextVideo"
|
||||
static let thotGenderOption = "thotGenderOption"
|
||||
}
|
||||
|
||||
@main
|
||||
struct Werkout_iosApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
@State var additionalWindows: [UIWindow] = []
|
||||
@State private var tabSelection = 1
|
||||
|
||||
let pub = NotificationCenter.default.publisher(for: NSNotification.Name("CreatedNewWorkout"))
|
||||
|
||||
private var screenDidConnectPublisher: AnyPublisher<UIScreen, Never> {
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIScreen.didConnectNotification)
|
||||
.compactMap { $0.object as? UIScreen }
|
||||
.receive(on: RunLoop.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var screenDidDisconnectPublisher: AnyPublisher<UIScreen, Never> {
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIScreen.didDisconnectNotification)
|
||||
.compactMap { $0.object as? UIScreen }
|
||||
.receive(on: RunLoop.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
TabView(selection: $tabSelection) {
|
||||
AllWorkoutsView()
|
||||
.onReceive(
|
||||
screenDidConnectPublisher,
|
||||
perform: screenDidConnect
|
||||
)
|
||||
.onReceive(
|
||||
screenDidDisconnectPublisher,
|
||||
perform: screenDidDisconnect
|
||||
)
|
||||
.tabItem {
|
||||
Label("All Workouts", systemImage: "figure.strengthtraining.traditional")
|
||||
}
|
||||
.tag(1)
|
||||
|
||||
CreateWorkoutMainView()
|
||||
.tabItem {
|
||||
Label("Create Workout", systemImage: "plus.app.fill")
|
||||
}
|
||||
.tag(2)
|
||||
|
||||
AccountView()
|
||||
.tabItem {
|
||||
Label("Accounts", systemImage: "person.fill.turn.down")
|
||||
}
|
||||
.tag(3)
|
||||
}
|
||||
.accentColor(Color("appColor"))
|
||||
.onAppear{
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
_ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
// UserStore.shared.logout()
|
||||
}
|
||||
.onReceive(pub) { (output) in
|
||||
self.tabSelection = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func screenDidDisconnect(_ screen: UIScreen) {
|
||||
additionalWindows.removeAll { $0.screen == screen }
|
||||
BridgeModule.shared.isShowingOnExternalDisplay = false
|
||||
}
|
||||
|
||||
private func screenDidConnect(_ screen: UIScreen) {
|
||||
let window = UIWindow(frame: screen.bounds)
|
||||
|
||||
window.windowScene = UIApplication.shared.connectedScenes
|
||||
.first { ($0 as? UIWindowScene)?.screen == screen }
|
||||
as? UIWindowScene
|
||||
|
||||
let view = ExternalWorkoutDetailView()
|
||||
.preferredColorScheme(.dark)
|
||||
let controller = UIHostingController(rootView: view)
|
||||
window.rootViewController = controller
|
||||
window.isHidden = false
|
||||
additionalWindows.append(window)
|
||||
BridgeModule.shared.isShowingOnExternalDisplay = true
|
||||
}
|
||||
}
|
||||
BIN
iphone/Werkout_ios/long_beep.m4a
Normal file
BIN
iphone/Werkout_ios/long_beep.m4a
Normal file
Binary file not shown.
BIN
iphone/Werkout_ios/short_beep.m4a
Normal file
BIN
iphone/Werkout_ios/short_beep.m4a
Normal file
Binary file not shown.
110
iphone/Werkout_ios/subview/ActionsView.swift
Normal file
110
iphone/Werkout_ios/subview/ActionsView.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// ActionsView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActionsView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
var completedWorkout: (() -> Void)?
|
||||
var planWorkout: ((Workout) -> Void)?
|
||||
|
||||
var workout: Workout
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var showAddToCalendar: Bool
|
||||
var startWorkoutAction: (() -> Void)
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if bridgeModule.isInWorkout == false {
|
||||
Button(action: {
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark.octagon.fill")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.red)
|
||||
.foregroundColor(.white)
|
||||
|
||||
if showAddToCalendar {
|
||||
Button(action: {
|
||||
planWorkout?(workout)
|
||||
}, label: {
|
||||
Image(systemName: "calendar.badge.plus")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.blue)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
startWorkoutAction()
|
||||
}, label: {
|
||||
Image(systemName: "arrowtriangle.forward.fill")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.green)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Button(action: {
|
||||
nextExercise()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.forward")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.green)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Button(action: {
|
||||
bridgeModule.pauseWorkout()
|
||||
}, label: {
|
||||
bridgeModule.isPaused ?
|
||||
Image(systemName: "play.circle.fill")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
:
|
||||
Image(systemName: "pause.circle.fill")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(bridgeModule.isPaused ? .mint : .yellow)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Button(action: {
|
||||
completedWorkout?()
|
||||
}, label: {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.blue)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextExercise() {
|
||||
bridgeModule.nextExercise()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ActionsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ActionsView(workout: PreviewData.workout(), showAddToCalendar: true, startWorkoutAction: {})
|
||||
}
|
||||
}
|
||||
56
iphone/Werkout_ios/subview/AllEquipmentView.swift
Normal file
56
iphone/Werkout_ios/subview/AllEquipmentView.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// AllEquipmentview.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllEquipmentView: View {
|
||||
@Binding var selectedEquipment: [Equipment]
|
||||
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let _ = DataStore.shared.allEquipment {
|
||||
Text("Select Equipment")
|
||||
.foregroundColor(.cyan)
|
||||
Text("\(selectedEquipment.count) Selected")
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if let equipment = DataStore.shared.allEquipment {
|
||||
var createWorkoutItemPickerModels = [CreateWorkoutItemPickerModel]()
|
||||
equipment.forEach({
|
||||
let model = CreateWorkoutItemPickerModel(id: $0.id,
|
||||
name: $0.name.lowercased())
|
||||
createWorkoutItemPickerModels.append(model)
|
||||
})
|
||||
createWorkoutItemPickerModels = createWorkoutItemPickerModels.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
let selectedIds = selectedEquipment.map { $0.id }
|
||||
createWorkoutItemPickerViewModel = CreateWorkoutItemPickerViewModel(allValues: createWorkoutItemPickerModels, selectedIds: selectedIds)
|
||||
}
|
||||
}
|
||||
.sheet(item: $createWorkoutItemPickerViewModel) { item in
|
||||
CreateWorkoutItemPickerView(viewModel: item, completed: { selectedids in
|
||||
if let equipment = DataStore.shared.allEquipment {
|
||||
selectedEquipment.removeAll()
|
||||
for id in selectedids {
|
||||
if let equipment = equipment.first(where: {
|
||||
$0.id == id
|
||||
}) {
|
||||
selectedEquipment.append(equipment)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// AllEquipmentview()
|
||||
//}
|
||||
92
iphone/Werkout_ios/subview/AllExerciseView.swift
Normal file
92
iphone/Werkout_ios/subview/AllExerciseView.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// AllExerciseView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct AllExerciseView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var searchString: String = ""
|
||||
@Binding var filteredExercises: [Exercise]
|
||||
var selectedExercise: ((Exercise) -> Void)
|
||||
|
||||
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
|
||||
@State var videoExercise: Exercise? {
|
||||
didSet {
|
||||
if let viddd = self.videoExercise?.videoURL,
|
||||
let url = URL(string: BaseURLs.currentBaseURL + viddd) {
|
||||
self.avPlayer = AVPlayer(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField("Filter", text: $searchString)
|
||||
.padding()
|
||||
|
||||
List() {
|
||||
ForEach(filteredExercises, id: \.self) { exercise in
|
||||
if searchString.isEmpty || (exercise.name.lowercased().contains(searchString.lowercased()) || (exercise.muscleGroups ?? "").lowercased().contains(searchString.lowercased())) {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(exercise.name)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if exercise.side != nil && !exercise.side!.isEmpty {
|
||||
Text(exercise.side!)
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if exercise.equipmentRequired != nil && !exercise.equipmentRequired!.isEmpty {
|
||||
Text(exercise.spacedEquipmentRequired)
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if exercise.muscleGroups != nil && !exercise.muscleGroups!.isEmpty {
|
||||
Text(exercise.spacedMuscleGroups)
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedExercise(exercise)
|
||||
dismiss()
|
||||
}
|
||||
Button(action: {
|
||||
videoExercise = exercise
|
||||
}) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.blue)
|
||||
.frame(width: 33, height: 33)
|
||||
Image(systemName: "video.fill")
|
||||
.frame(width: 33, height: 33)
|
||||
.foregroundColor(.white )
|
||||
}
|
||||
}
|
||||
.frame(width: 33, height: 33)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $videoExercise) { exercise in
|
||||
PlayerView(player: $avPlayer)
|
||||
.onAppear{
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//#Preview {
|
||||
// AllExerciseView()
|
||||
//}
|
||||
66
iphone/Werkout_ios/subview/AllMusclesView.swift
Normal file
66
iphone/Werkout_ios/subview/AllMusclesView.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// AllMusclesView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllMusclesView: View {
|
||||
@Binding var selectedMuscles: [Muscle]
|
||||
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let _ = DataStore.shared.allMuscles {
|
||||
Text("Select Muscles")
|
||||
.foregroundColor(.cyan)
|
||||
Text("\(selectedMuscles.count) Selected")
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if let muscles = DataStore.shared.allMuscles {
|
||||
var createWorkoutItemPickerModels = [CreateWorkoutItemPickerModel]()
|
||||
muscles.forEach({
|
||||
let model = CreateWorkoutItemPickerModel(id: $0.id, name: $0.name.lowercased())
|
||||
createWorkoutItemPickerModels.append(model)
|
||||
})
|
||||
createWorkoutItemPickerModels = createWorkoutItemPickerModels.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
let selectedIds = selectedMuscles.map { $0.id }
|
||||
createWorkoutItemPickerViewModel = CreateWorkoutItemPickerViewModel(allValues: createWorkoutItemPickerModels, selectedIds: selectedIds)
|
||||
}
|
||||
}
|
||||
.onAppear{
|
||||
if #function.hasPrefix("__preview") {
|
||||
DataStore.shared.setupFakeData()
|
||||
}
|
||||
guard let _ = DataStore.shared.allExercise,
|
||||
let muscles = DataStore.shared.allMuscles,
|
||||
let _ = DataStore.shared.allEquipment else {
|
||||
return
|
||||
}
|
||||
selectedMuscles = muscles
|
||||
}
|
||||
.sheet(item: $createWorkoutItemPickerViewModel) { item in
|
||||
CreateWorkoutItemPickerView(viewModel: item, completed: { selectedids in
|
||||
if let muscles = DataStore.shared.allMuscles {
|
||||
selectedMuscles.removeAll()
|
||||
for id in selectedids {
|
||||
if let muscle = muscles.first(where: {
|
||||
$0.id == id
|
||||
}) {
|
||||
selectedMuscles.append(muscle)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// AllMusclesView()
|
||||
//}
|
||||
37
iphone/Werkout_ios/subview/AllWorkoutPickerView.swift
Normal file
37
iphone/Werkout_ios/subview/AllWorkoutPickerView.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// AllWorkoutPickerView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllWorkoutPickerView: View {
|
||||
var mainViews: [MainViewTypes]
|
||||
@Binding var selectedSegment: MainViewTypes
|
||||
@StateObject var bridgeModule = BridgeModule.shared
|
||||
var showCurrentWorkout: (() -> Void)
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Picker("", selection: $selectedSegment) {
|
||||
ForEach(mainViews, id: \.self) { viewType in
|
||||
Text(viewType.title)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding([.top, .leading, .trailing])
|
||||
|
||||
if bridgeModule.isInWorkout {
|
||||
Button(action: {
|
||||
showCurrentWorkout()
|
||||
}, label: {
|
||||
Image(systemName: "figure.strengthtraining.traditional")
|
||||
.padding(.trailing)
|
||||
})
|
||||
.tint(Color("appColor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
iphone/Werkout_ios/subview/CompletedWorkoutsView.swift
Normal file
71
iphone/Werkout_ios/subview/CompletedWorkoutsView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// CompletedWorkoutsView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompletedWorkoutsView: View {
|
||||
@State var completedWorkouts: [CompletedWorkout]?
|
||||
@State var showCompletedWorkouts: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if let completedWorkouts = completedWorkouts {
|
||||
|
||||
Divider()
|
||||
Text("Workout History:")
|
||||
HStack {
|
||||
Text("Number of workouts:")
|
||||
Text("\(completedWorkouts.count)")
|
||||
}
|
||||
|
||||
if let lastWorkout = completedWorkouts.last {
|
||||
HStack {
|
||||
Text("Last workout:")
|
||||
Text(lastWorkout.workoutStartTime)
|
||||
}
|
||||
|
||||
Button("View All Workouts", action: {
|
||||
showCompletedWorkouts = true
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.blue)
|
||||
.background(.yellow)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
} else {
|
||||
Text("loading completed workouts")
|
||||
}
|
||||
}
|
||||
.onAppear{
|
||||
fetchCompletedWorkouts()
|
||||
}
|
||||
.sheet(isPresented: $showCompletedWorkouts) {
|
||||
if let completedWorkouts = completedWorkouts {
|
||||
WorkoutHistoryView(completedWorkouts: completedWorkouts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCompletedWorkouts() {
|
||||
CompletedWorkoutFetchable().fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
completedWorkouts = model
|
||||
case .failure(let failure):
|
||||
fatalError(failure.localizedDescription)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompletedWorkoutsView()
|
||||
}
|
||||
30
iphone/Werkout_ios/subview/CountdownView.swift
Normal file
30
iphone/Werkout_ios/subview/CountdownView.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// CountdownView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CountdownView: View {
|
||||
@StateObject var bridgeModule = BridgeModule.shared
|
||||
|
||||
var body: some View {
|
||||
if let duration = bridgeModule.currentExerciseInfo.currentExercise?.duration,
|
||||
duration > 0 {
|
||||
HStack {
|
||||
if bridgeModule.currentExerciseTimeLeft >= 0 && duration > bridgeModule.currentExerciseTimeLeft {
|
||||
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
|
||||
.tint(Color("appColor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CountdownView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CountdownView()
|
||||
}
|
||||
}
|
||||
81
iphone/Werkout_ios/subview/ExtCountdownView.swift
Normal file
81
iphone/Werkout_ios/subview/ExtCountdownView.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// ExtCountdownView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExtCountdownView: View {
|
||||
@StateObject var bridgeModule = BridgeModule.shared
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { metrics in
|
||||
VStack {
|
||||
if let currenExercise = bridgeModule.currentExerciseInfo.currentExercise {
|
||||
HStack {
|
||||
Text(currenExercise.exercise.extName)
|
||||
.font(.system(size: 200))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
|
||||
Text("\(Double(bridgeModule.currentWorkoutRunTimeInSeconds).asString(style: .positional))")
|
||||
.font(Font.system(size: 100))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.trailing, 100)
|
||||
}
|
||||
}
|
||||
.frame(height: metrics.size.height * 0.5)
|
||||
|
||||
HStack {
|
||||
if let duration = currenExercise.duration,
|
||||
duration > 0 {
|
||||
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
|
||||
.scaleEffect(x: 1, y: 6, anchor: .center)
|
||||
Text("\(bridgeModule.currentExerciseTimeLeft)")
|
||||
.font(Font.system(size: 100))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.padding(.leading)
|
||||
.padding(.trailing, 100)
|
||||
}
|
||||
|
||||
if let reps = currenExercise.reps,
|
||||
reps > 0 {
|
||||
Text(" X \(reps)")
|
||||
.font(Font.system(size: 100))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if let weight = currenExercise.weight,
|
||||
weight > 0 {
|
||||
Text(" @ \(weight)")
|
||||
.font(Font.system(size: 100))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.padding(.trailing, 100)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(height: metrics.size.height * 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExtCountdownView()
|
||||
}
|
||||
76
iphone/Werkout_ios/subview/ExtExerciseList.swift
Normal file
76
iphone/Werkout_ios/subview/ExtExerciseList.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// ExtExerciseList.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExtExerciseList: View {
|
||||
var workout: Workout
|
||||
var allSupersetExecerciseIndex: Int
|
||||
|
||||
var body: some View {
|
||||
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: "figure.run")
|
||||
.foregroundColor(Color("appColor"))
|
||||
.font(Font.system(size: 55))
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Text(supersetExecercise.exercise.name)
|
||||
.font(Font.system(size: 55))
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(3)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// ExtExerciseList()
|
||||
//}
|
||||
42
iphone/Werkout_ios/subview/InfoView.swift
Normal file
42
iphone/Werkout_ios/subview/InfoView.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// InfoView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InfoView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
var workout: Workout
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(workout.name)
|
||||
.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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InfoView(workout: PreviewData.workout())
|
||||
}
|
||||
}
|
||||
43
iphone/Werkout_ios/subview/Logoutview.swift
Normal file
43
iphone/Werkout_ios/subview/Logoutview.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Logoutview.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Logoutview: View {
|
||||
@ObservedObject var userStore = UserStore.shared
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button("Logout", action: {
|
||||
userStore.logout()
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.red)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Button(action: {
|
||||
userStore.refreshUserData()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
})
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(.green)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Logoutview()
|
||||
}
|
||||
34
iphone/Werkout_ios/subview/NameView.swift
Normal file
34
iphone/Werkout_ios/subview/NameView.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// NameView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NameView: View {
|
||||
@ObservedObject var userStore = UserStore.shared
|
||||
|
||||
var body: some View {
|
||||
if let registeredUser = userStore.registeredUser {
|
||||
if let nickName = registeredUser.nickName {
|
||||
Text(nickName)
|
||||
.font(.title)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(registeredUser.firstName ?? "-")
|
||||
Text(registeredUser.lastName ?? "-")
|
||||
}
|
||||
|
||||
if let email = registeredUser.email {
|
||||
Text(email)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NameView()
|
||||
}
|
||||
18
iphone/Werkout_ios/subview/OvalTextFieldStyle.swift
Normal file
18
iphone/Werkout_ios/subview/OvalTextFieldStyle.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// OvalTextFieldStyle.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/6/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OvalTextFieldStyle: TextFieldStyle {
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
configuration
|
||||
.padding(10)
|
||||
.background(LinearGradient(gradient: Gradient(colors: [Color(uiColor: .secondarySystemBackground), Color(uiColor: .secondarySystemBackground)]), startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.cornerRadius(20)
|
||||
.shadow(color: Color(red: 120/255, green: 120/255, blue: 120/255, opacity: 1), radius: 5)
|
||||
}
|
||||
}
|
||||
127
iphone/Werkout_ios/subview/PlayerUIView.swift
Normal file
127
iphone/Werkout_ios/subview/PlayerUIView.swift
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// PlayerUIView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/5/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
class PlayerUIView: UIView {
|
||||
|
||||
// MARK: Class Property
|
||||
|
||||
let playerLayer = AVPlayerLayer()
|
||||
|
||||
// MARK: Init
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
init(player: AVPlayer) {
|
||||
super.init(frame: .zero)
|
||||
self.playerSetup(player: player)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Life-Cycle
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
playerLayer.frame = bounds
|
||||
}
|
||||
|
||||
// MARK: Class Methods
|
||||
|
||||
private func playerSetup(player: AVPlayer) {
|
||||
playerLayer.player = player
|
||||
player.actionAtItemEnd = .none
|
||||
layer.addSublayer(playerLayer)
|
||||
|
||||
self.setObserver()
|
||||
}
|
||||
|
||||
func setObserver() {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(notification:)),
|
||||
name: .AVPlayerItemDidPlayToEndTime,
|
||||
object: playerLayer.player?.currentItem)
|
||||
}
|
||||
|
||||
@objc func playerItemDidReachEnd(notification: Notification) {
|
||||
if let playerItem = notification.object as? AVPlayerItem {
|
||||
playerItem.seek(to: .zero, completionHandler: nil)
|
||||
self.playerLayer.player?.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerView: UIViewRepresentable {
|
||||
|
||||
@Binding var player: AVPlayer
|
||||
|
||||
func makeUIView(context: Context) -> PlayerUIView {
|
||||
return PlayerUIView(player: player)
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
|
||||
uiView.playerLayer.player = player
|
||||
|
||||
//Add player observer.
|
||||
uiView.setObserver()
|
||||
}
|
||||
}
|
||||
|
||||
class VideoURLCreator {
|
||||
class func otherVideoType(forVideoURL videoURL: URL) -> ThotStyle {
|
||||
var otherVideoStyle = ThotStyle.never
|
||||
if videoURL.absoluteString.contains("exercise_videos") {
|
||||
otherVideoStyle = .always
|
||||
}
|
||||
return otherVideoStyle
|
||||
}
|
||||
|
||||
class func videoURL(thotStyle: ThotStyle, gender: String, defaultVideoURLStr: String?, exerciseName: String?, workout: Workout?) -> URL? {
|
||||
var urlString: String?
|
||||
|
||||
if UserStore.shared.registeredUser?.NSFWValue ?? false {
|
||||
switch thotStyle {
|
||||
case .always:
|
||||
urlString = DataStore.shared.randomVideoFor(gender: gender)
|
||||
case .never:
|
||||
urlString = defaultVideoURLStr
|
||||
case .recovery:
|
||||
if exerciseName?.lowercased() == "recover" {
|
||||
urlString = DataStore.shared.randomVideoFor(gender: gender)
|
||||
} else {
|
||||
urlString = defaultVideoURLStr
|
||||
}
|
||||
case .random:
|
||||
if Bool.random() {
|
||||
urlString = DataStore.shared.randomVideoFor(gender: gender)
|
||||
} else {
|
||||
urlString = defaultVideoURLStr
|
||||
}
|
||||
case .off:
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
urlString = defaultVideoURLStr
|
||||
}
|
||||
|
||||
if let urlString = urlString,
|
||||
let url = URL(string: BaseURLs.currentBaseURL + urlString) {
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
22
iphone/Werkout_ios/subview/ShowNextUpView.swift
Normal file
22
iphone/Werkout_ios/subview/ShowNextUpView.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// ShowNextUpView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShowNextUpView: View {
|
||||
@AppStorage(Constants.extShowNextVideo) private var extShowNextVideo: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: $extShowNextVideo, label: {
|
||||
Text("Show next up video")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ShowNextUpView()
|
||||
}
|
||||
57
iphone/Werkout_ios/subview/ThotPreferenceView.swift
Normal file
57
iphone/Werkout_ios/subview/ThotPreferenceView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// ThosPreferenceView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ThotPreferenceView: View {
|
||||
@ObservedObject var userStore = UserStore.shared
|
||||
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
|
||||
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
|
||||
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
|
||||
|
||||
var body: some View {
|
||||
if userStore.registeredUser?.NSFWValue ?? false {
|
||||
Group {
|
||||
Text("Phone THOT Style:")
|
||||
Picker("Phone THOT Style:", selection: $phoneThotStyle) {
|
||||
ForEach(ThotStyle.allCases, id: \.self) { style in
|
||||
Text(style.stringValue())
|
||||
.tag(phoneThotStyle.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Divider()
|
||||
|
||||
Text("External THOT Style:")
|
||||
Picker("External THOT Style:", selection: $extThotStyle) {
|
||||
ForEach(ThotStyle.allCases, id: \.self) { style in
|
||||
Text(style.stringValue())
|
||||
.tag(extThotStyle.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
if let genderOptions = DataStore.shared.nsfwGenderOptions {
|
||||
Divider()
|
||||
Text("Video Gender:")
|
||||
Picker("Video Gender:", selection: $thotGenderOption) {
|
||||
ForEach(genderOptions, id: \.self) { option in
|
||||
Text(option.capitalized)
|
||||
.tag(option.lowercased())
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ThotPreferenceView()
|
||||
}
|
||||
33
iphone/Werkout_ios/subview/TitleView.swift
Normal file
33
iphone/Werkout_ios/subview/TitleView.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// TitleView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TitleView: View {
|
||||
@ObservedObject var bridgeModule = BridgeModule.shared
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let workout = bridgeModule.currentExerciseInfo.workout {
|
||||
Text(workout.name)
|
||||
.font(Font.system(size: 100))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
|
||||
Text("\(bridgeModule.currentWorkoutRunTimeInSeconds)")
|
||||
.font(Font.system(size: 100))
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.trailing, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TitleView()
|
||||
}
|
||||
71
iphone/Werkout_ios/subview/WorkoutOverviewView.swift
Normal file
71
iphone/Werkout_ios/subview/WorkoutOverviewView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// WorkoutOverviewView.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/9/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WorkoutOverviewView: View {
|
||||
let workout: Workout
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(workout.name)
|
||||
.font(.title2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
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 {
|
||||
Text("\(exerciseCount)")
|
||||
.font(.body.bold())
|
||||
Text("exercises")
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color(uiColor: .systemGray2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let muscles = workout.muscles,
|
||||
muscles.joined(separator: ", ").count > 0{
|
||||
Divider()
|
||||
Text(muscles.joined(separator: ", "))
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if let equipment = workout.equipment,
|
||||
equipment.joined(separator: ", ").count > 0 {
|
||||
Divider()
|
||||
Text(equipment.joined(separator: ", "))
|
||||
.font(.footnote)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(15)
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkoutOverviewView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WorkoutOverviewView(workout: PreviewData.allWorkouts()[2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo_2.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
25
iphone/Werkout_watch Watch App/ContentView.swift
Normal file
25
iphone/Werkout_watch Watch App/ContentView.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Werkout_watch Watch App
|
||||
//
|
||||
// Created by Trey Tartt on 6/22/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
MainWatchView()
|
||||
WatchControlView()
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
75
iphone/Werkout_watch Watch App/MainWatchView.swift
Normal file
75
iphone/Werkout_watch Watch App/MainWatchView.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// MainView.swift
|
||||
// Werkout_watch Watch App
|
||||
//
|
||||
// Created by Trey Tartt on 7/8/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MainWatchView: View {
|
||||
@StateObject var vm = WatchMainViewModel.shared
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
if vm.isInWorkout {
|
||||
Text(vm.watchPackageModel.currentExerciseName)
|
||||
.font(.body)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(10)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Divider()
|
||||
Text("\(vm.watchPackageModel.currentTimeLeft )")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(10)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
if let heartValue = vm.heartValue {
|
||||
VStack {
|
||||
Image(systemName: "heart.fill")
|
||||
.font(Font.system(size: 22))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.red)
|
||||
Text("\(heartValue)")
|
||||
.font(Font.system(size: 55))
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.01)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
if vm.isInWorkout {
|
||||
Button(action: {
|
||||
vm.nextExercise()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.forward")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
})
|
||||
.buttonStyle(BorderedButtonStyle(tint: .green))
|
||||
} else {
|
||||
VStack {
|
||||
Text("No Werkout")
|
||||
Text("🍑")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainWatchView()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user