You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

269 lines
7.5 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. import "./ProgramBoi.sass";
  2. import {useContext, useEffect, useMemo, useReducer} from "react";
  3. import RuntimeContext from "../../contexts/RuntimeContext";
  4. import {ProgramStep, weighting} from "../../models/Programs";
  5. import {firstKey, WorkoutState} from "../../models/Workouts";
  6. import {diffLinearValues, formatValue} from "../../models/Shared";
  7. import {Boi} from "../../primitives/boi/Boi";
  8. import {useLastMilestoneValue} from "../../hooks/milestones";
  9. interface StepMeta {
  10. actualDuration?: WorkoutState
  11. }
  12. interface ProgressState {
  13. steps: (ProgramStep & StepMeta)[]
  14. currentIndex: number
  15. lastTransition: WorkoutState
  16. lastValue: WorkoutState
  17. toNext: ToNext
  18. stopped: boolean
  19. }
  20. interface ToNext {
  21. current: number,
  22. max: number,
  23. }
  24. interface ProgressChange {
  25. skip?: boolean
  26. workoutState?: WorkoutState
  27. }
  28. function calculateToNext(
  29. step: ProgramStep & StepMeta,
  30. lastValue: WorkoutState,
  31. lastTransition: WorkoutState,
  32. ): ToNext {
  33. if (step.duration) {
  34. if (step.duration.time) {
  35. return {
  36. current: lastValue.time - lastTransition.time,
  37. max: step.duration.time,
  38. };
  39. } else if (step.duration.calories && lastTransition.calories !== undefined && lastValue.calories !== undefined) {
  40. return {
  41. current: lastValue.calories - lastTransition.calories,
  42. max: step.duration.calories,
  43. };
  44. } else if (step.duration.distance && lastTransition.distance !== undefined && lastValue.distance !== undefined) {
  45. return {
  46. current: lastValue.distance - lastTransition.distance,
  47. max: step.duration.distance,
  48. };
  49. }
  50. }
  51. return {
  52. current: 0,
  53. max: 1,
  54. }
  55. }
  56. function programReducer(state: ProgressState, change: ProgressChange) {
  57. let {steps, currentIndex, lastTransition, lastValue, toNext, stopped} = state;
  58. // Stop working if after program
  59. if (stopped) {
  60. return state;
  61. } else if (currentIndex > steps.length - 1) {
  62. return {...state, stopped: true};
  63. }
  64. // Skip
  65. if (change.skip) {
  66. steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
  67. return {
  68. ...state,
  69. steps,
  70. lastTransition: lastValue,
  71. currentIndex: currentIndex + 1,
  72. };
  73. }
  74. // Workout state
  75. if (change.workoutState) {
  76. lastValue = change.workoutState;
  77. const step = steps[currentIndex];
  78. if (step.duration) {
  79. toNext = calculateToNext(step, lastValue, lastTransition);
  80. if (toNext.current >= toNext.max) {
  81. steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
  82. currentIndex += 1;
  83. lastTransition = lastValue;
  84. if (steps.length > currentIndex) {
  85. toNext = calculateToNext(steps[currentIndex], lastValue, lastTransition);
  86. }
  87. }
  88. }
  89. }
  90. return {steps, currentIndex, lastTransition, lastValue, toNext, stopped};
  91. }
  92. export default function ProgramBoi() {
  93. const {workout, lastEvent} = useContext(RuntimeContext);
  94. const program = workout!.program!;
  95. const [progress, dispatch] = useReducer(programReducer, {
  96. steps: program.steps,
  97. currentIndex: 0,
  98. lastTransition: {time: 0, distance: 0, calories: 0},
  99. lastValue: {time: 0},
  100. toNext: {current: 0, max: 1},
  101. stopped: false,
  102. });
  103. useEffect(() => {
  104. if (lastEvent) {
  105. for (const workoutState of lastEvent?.workoutStates || []) {
  106. dispatch({workoutState});
  107. }
  108. if (lastEvent?.event?.name === "Skipped") {
  109. dispatch({skip: true});
  110. }
  111. }
  112. }, [lastEvent]);
  113. if (progress.steps.some(s => weighting(s) === 0)) {
  114. return <InfiniteProgress progress={progress}/>;
  115. }
  116. return <HealthBarProgress progress={progress}/>;
  117. }
  118. interface ProgressProps {
  119. progress: ProgressState
  120. }
  121. type FromTo = {
  122. count: false
  123. } | {
  124. count: true
  125. rawCurr: number
  126. rawTo: number
  127. strCurr: string
  128. strTo: string
  129. }
  130. function InfiniteProgress({progress}: ProgressProps) {
  131. const offset = 6 - progress.steps.length + progress.currentIndex;
  132. const ft: FromTo = useMemo(() => {
  133. const currentStep = progress.steps[progress.currentIndex];
  134. if (currentStep) {
  135. const key = firstKey(currentStep.duration as WorkoutState);
  136. if (key !== null) {
  137. const duration = progress.steps[progress.currentIndex].duration![key]!;
  138. const numStr = formatValue(progress.toNext.current, key);
  139. const toStr = formatValue(duration, key);
  140. return {
  141. count: true,
  142. rawCurr: progress.toNext.current,
  143. rawTo: duration,
  144. strCurr: numStr,
  145. strTo: toStr,
  146. } as FromTo
  147. }
  148. }
  149. return {count: false};
  150. }, [progress]);
  151. if (progress.currentIndex >= progress.steps.length) {
  152. return null;
  153. }
  154. return (
  155. <Boi
  156. vertical="bottom" horizontal="center" unchunky
  157. style={{fontSize: "2vmax", minWidth: "24vmax"}}
  158. >
  159. <div style={{padding: "0.5vmax 0.5vmax 0", fontWeight: 400}}>
  160. Steg {progress.currentIndex + 1} / {progress.steps.length}
  161. </div>
  162. {ft.count && (
  163. <div>
  164. <div style={{margin: "0.5vmax"}}>
  165. {ft.strCurr} / {ft.strTo}
  166. </div>
  167. <div className="ProgressBar">
  168. <div className={`ProgressBar-level-${offset}`} style={{flex: ft.rawCurr}}/>
  169. <div className={`ProgressBar-bg`} style={{flex: ft.rawTo - ft.rawCurr}}/>
  170. </div>
  171. </div>
  172. )}
  173. {!ft.count && (
  174. <div style={{margin: "0.5vmax"}}>
  175. (Custom)
  176. </div>
  177. )}
  178. </Boi>
  179. );
  180. }
  181. function HealthBarProgress({progress}: ProgressProps) {
  182. const offset = 6 - progress.steps.length;
  183. const steps = progress.steps.reduce((acc, b) => ([b, ...acc]), [] as typeof progress.steps);
  184. return (
  185. <div className="HealthBar">
  186. {steps.map((step, i) => <HealthBarProgressStep key={i} progress={progress} step={step} offset={offset}/>)}
  187. </div>
  188. );
  189. }
  190. interface HealthBarProgressStepProps {
  191. progress: ProgressState
  192. step: ProgramStep & StepMeta
  193. offset: number
  194. }
  195. function HealthBarProgressStep({progress, step, offset}: HealthBarProgressStepProps) {
  196. const stepIndex = progress.steps.indexOf(step);
  197. const level = progress.steps.indexOf(step) + offset;
  198. const max = Math.max(1, progress.toNext.max);
  199. const key = firstKey(step.duration as WorkoutState);
  200. const lastMilestone = useLastMilestoneValue(key!, "time");
  201. const duration = step.duration![key!]!;
  202. const durationStr = formatValue(duration - progress.toNext.current, key!);
  203. const currentVal = Math.max(0, progress.toNext.current);
  204. const preDiffed = key !== "time" ? Math.max(0, (progress.lastValue[key!] || 0) - lastMilestone) : -1;
  205. const diffed = Math.min(preDiffed, currentVal);
  206. return (
  207. <div key={step.index} className="HealthBar-entry" style={{flex: weighting(step)}}>
  208. {stepIndex === progress.currentIndex && (
  209. <div className="HealthBar-entry-text">
  210. {durationStr}
  211. </div>
  212. )}
  213. <div className="ProgressBar">
  214. {stepIndex > progress.currentIndex && (
  215. <div className={`ProgressBar-inactive`} style={{flex: 1}}/>
  216. )}
  217. {stepIndex === progress.currentIndex && (
  218. <>
  219. <div className={`ProgressBar-level-${level}`} style={{flex: max - currentVal}}/>
  220. {diffed >= 0 && (
  221. <div className={`ProgressBar-level-${level} diff`} style={{flex: diffed}}/>
  222. )}
  223. <div className={`ProgressBar-bg`} style={{flex: currentVal - diffed}}/>
  224. </>
  225. )}
  226. {stepIndex < progress.currentIndex && (
  227. <div className={`ProgressBar-bg`} style={{flex: 1}}/>
  228. )}
  229. </div>
  230. </div>
  231. );
  232. }