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.
222 lines
6.5 KiB
222 lines
6.5 KiB
import "./ProgramBoi.sass";
|
|
|
|
import {useContext, useEffect, useMemo, useReducer} from "react";
|
|
import RuntimeContext from "../../contexts/RuntimeContext";
|
|
import {ProgramStep, weighting} from "../../models/Programs";
|
|
import {firstKey, WorkoutState} from "../../models/Workouts";
|
|
import {diffLinearValues, formatValue} from "../../models/Shared";
|
|
import {Boi} from "../../primitives/boi/Boi";
|
|
|
|
interface StepMeta {
|
|
actualDuration?: WorkoutState
|
|
}
|
|
|
|
interface ProgressState {
|
|
steps: (ProgramStep & StepMeta)[]
|
|
currentIndex: number
|
|
lastTransition: WorkoutState
|
|
lastValue: WorkoutState
|
|
toNext: { current: number, max: number }
|
|
stopped: boolean
|
|
}
|
|
|
|
interface ProgressChange {
|
|
skip?: boolean
|
|
workoutState?: WorkoutState
|
|
}
|
|
|
|
function programReducer(state: ProgressState, change: ProgressChange) {
|
|
let {steps, currentIndex, lastTransition, lastValue, toNext, stopped} = state;
|
|
|
|
// Stop working if after program
|
|
if (stopped) {
|
|
return state;
|
|
} else if (currentIndex > steps.length - 1) {
|
|
return {...state, stopped: true};
|
|
}
|
|
|
|
// Skip
|
|
if (change.skip) {
|
|
steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
|
|
|
|
return {
|
|
...state,
|
|
steps,
|
|
lastTransition: lastValue,
|
|
currentIndex: currentIndex + 1,
|
|
};
|
|
}
|
|
|
|
// Workout state
|
|
if (change.workoutState) {
|
|
lastValue = change.workoutState;
|
|
const step = steps[currentIndex];
|
|
|
|
if (step.duration) {
|
|
if (step.duration.time) {
|
|
toNext.current = lastValue.time - lastTransition.time;
|
|
toNext.max = step.duration.time;
|
|
} else if (step.duration.calories && lastTransition.calories !== undefined && lastValue.calories !== undefined) {
|
|
toNext.current = lastValue.calories - lastTransition.calories;
|
|
toNext.max = step.duration.calories;
|
|
} else if (step.duration.distance && lastTransition.distance !== undefined && lastValue.distance !== undefined) {
|
|
toNext.current = lastValue.distance - lastTransition.distance;
|
|
toNext.max = step.duration.distance;
|
|
}
|
|
|
|
if (toNext.current >= toNext.max) {
|
|
steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
|
|
currentIndex += 1;
|
|
lastTransition = lastValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {steps, currentIndex, lastTransition, lastValue, toNext, stopped};
|
|
}
|
|
|
|
export default function ProgramBoi() {
|
|
const {workout, lastEvent} = useContext(RuntimeContext);
|
|
const program = workout!.program!;
|
|
const [progress, dispatch] = useReducer(programReducer, {
|
|
steps: program.steps,
|
|
currentIndex: 0,
|
|
lastTransition: {time: 0, distance: 0, calories: 0},
|
|
lastValue: {time: 0},
|
|
toNext: {current: 0, max: 1},
|
|
stopped: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (lastEvent) {
|
|
for (const workoutState of lastEvent?.workoutStates || []) {
|
|
dispatch({workoutState});
|
|
}
|
|
|
|
if (lastEvent?.event?.name === "Skipped") {
|
|
dispatch({skip: true});
|
|
}
|
|
}
|
|
}, [lastEvent]);
|
|
|
|
if (progress.steps.some(s => weighting(s) === 0)) {
|
|
return <InfiniteProgress progress={progress}/>;
|
|
}
|
|
|
|
return <HealthBarProgress progress={progress}/>;
|
|
}
|
|
|
|
interface ProgressProps {
|
|
progress: ProgressState
|
|
}
|
|
|
|
type FromTo = {
|
|
count: false
|
|
} | {
|
|
count: true
|
|
rawCurr: number
|
|
rawTo: number
|
|
strCurr: string
|
|
strTo: string
|
|
}
|
|
|
|
function InfiniteProgress({progress}: ProgressProps) {
|
|
const offset = 6 - progress.steps.length + progress.currentIndex;
|
|
const ft: FromTo = useMemo(() => {
|
|
const currentStep = progress.steps[progress.currentIndex];
|
|
if (currentStep) {
|
|
const key = firstKey(currentStep.duration as WorkoutState);
|
|
if (key !== null) {
|
|
const duration = progress.steps[progress.currentIndex].duration![key]!;
|
|
const numStr = formatValue(progress.toNext.current, key);
|
|
const toStr = formatValue(duration, key);
|
|
|
|
return {
|
|
count: true,
|
|
rawCurr: progress.toNext.current,
|
|
rawTo: duration,
|
|
strCurr: numStr,
|
|
strTo: toStr,
|
|
} as FromTo
|
|
}
|
|
}
|
|
|
|
return {count: false};
|
|
}, [progress]);
|
|
|
|
if (progress.currentIndex >= progress.steps.length) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Boi
|
|
vertical="bottom" horizontal="center" unchunky
|
|
style={{fontSize: "2vmax", minWidth: "24vmax"}}
|
|
>
|
|
<div style={{padding: "0.5vmax 0.5vmax 0", fontWeight: 400}}>
|
|
Steg {progress.currentIndex + 1} / {progress.steps.length}
|
|
</div>
|
|
{ft.count && (
|
|
<div>
|
|
<div style={{margin: "0.5vmax"}}>
|
|
{ft.strCurr} / {ft.strTo}
|
|
</div>
|
|
<div className="ProgressBar">
|
|
<div className={`ProgressBar-level-${offset}`} style={{flex: ft.rawCurr}}/>
|
|
<div className={`ProgressBar-bg`} style={{flex: ft.rawTo - ft.rawCurr}}/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{!ft.count && (
|
|
<div style={{margin: "0.5vmax"}}>
|
|
(Custom)
|
|
</div>
|
|
)}
|
|
</Boi>
|
|
);
|
|
}
|
|
|
|
function HealthBarProgress({progress}: ProgressProps) {
|
|
const offset = 6 - progress.steps.length;
|
|
const steps = progress.steps.reduce((acc, b) => ([b, ...acc]), [] as typeof progress.steps);
|
|
|
|
return (
|
|
<div className="HealthBar">
|
|
{steps.map((step) => {
|
|
const stepIndex = progress.steps.indexOf(step);
|
|
const level = progress.steps.indexOf(step) + offset;
|
|
|
|
const max = Math.max(1, progress.toNext.max);
|
|
const key = firstKey(step.duration as WorkoutState);
|
|
|
|
const duration = step.duration![key!]!;
|
|
|
|
const durationStr = formatValue(duration - progress.toNext.current, key!);
|
|
|
|
return (
|
|
<div key={step.index} className="HealthBar-entry" style={{flex: weighting(step)}}>
|
|
{stepIndex === progress.currentIndex && (
|
|
<div className="HealthBar-entry-text">
|
|
{durationStr}
|
|
</div>
|
|
)}
|
|
<div className="ProgressBar">
|
|
{stepIndex > progress.currentIndex && (
|
|
<div className={`ProgressBar-inactive`} style={{flex: 1}}/>
|
|
)}
|
|
{stepIndex === progress.currentIndex && (
|
|
<>
|
|
<div className={`ProgressBar-level-${level}`} style={{flex: max - progress.toNext.current}}/>
|
|
<div className={`ProgressBar-bg`} style={{flex: progress.toNext.current}}/>
|
|
</>
|
|
)}
|
|
{stepIndex < progress.currentIndex && (
|
|
<div className={`ProgressBar-bg`} style={{flex: 1}}/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|