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

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";
import {useLastMilestoneValue} from "../../hooks/milestones";
interface StepMeta {
actualDuration?: WorkoutState
}
interface ProgressState {
steps: (ProgramStep & StepMeta)[]
currentIndex: number
lastTransition: WorkoutState
lastValue: WorkoutState
toNext: ToNext
stopped: boolean
}
interface ToNext {
current: number,
max: number,
}
interface ProgressChange {
skip?: boolean
workoutState?: WorkoutState
}
function calculateToNext(
step: ProgramStep & StepMeta,
lastValue: WorkoutState,
lastTransition: WorkoutState,
): ToNext {
if (step.duration) {
if (step.duration.time) {
return {
current: lastValue.time - lastTransition.time,
max: step.duration.time,
};
} else if (step.duration.calories && lastTransition.calories !== undefined && lastValue.calories !== undefined) {
return {
current: lastValue.calories - lastTransition.calories,
max: step.duration.calories,
};
} else if (step.duration.distance && lastTransition.distance !== undefined && lastValue.distance !== undefined) {
return {
current: lastValue.distance - lastTransition.distance,
max: step.duration.distance,
};
}
}
return {
current: 0,
max: 1,
}
}
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) {
toNext = calculateToNext(step, lastValue, lastTransition);
if (toNext.current >= toNext.max) {
steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
currentIndex += 1;
lastTransition = lastValue;
if (steps.length > currentIndex) {
toNext = calculateToNext(steps[currentIndex], lastValue, lastTransition);
}
}
}
}
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, i) => <HealthBarProgressStep key={i} progress={progress} step={step} offset={offset}/>)}
</div>
);
}
interface HealthBarProgressStepProps {
progress: ProgressState
step: ProgramStep & StepMeta
offset: number
}
function HealthBarProgressStep({progress, step, offset}: HealthBarProgressStepProps) {
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 lastMilestone = useLastMilestoneValue(key!, "time");
const duration = step.duration![key!]!;
const durationStr = formatValue(duration - progress.toNext.current, key!);
const currentVal = Math.max(0, progress.toNext.current);
const preDiffed = key !== "time" ? Math.max(0, (progress.lastValue[key!] || 0) - lastMilestone) : -1;
const diffed = Math.min(preDiffed, currentVal);
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 - currentVal}}/>
{diffed >= 0 && (
<div className={`ProgressBar-level-${level} diff`} style={{flex: diffed}}/>
)}
<div className={`ProgressBar-bg`} style={{flex: currentVal - diffed}}/>
</>
)}
{stepIndex < progress.currentIndex && (
<div className={`ProgressBar-bg`} style={{flex: 1}}/>
)}
</div>
</div>
);
}