diff --git a/webui-react/src/hooks/kpm.ts b/webui-react/src/hooks/kpm.ts
new file mode 100644
index 0000000..80d386f
--- /dev/null
+++ b/webui-react/src/hooks/kpm.ts
@@ -0,0 +1,52 @@
+import {useContext, useEffect, useReducer} from "react";
+import RuntimeContext from "../contexts/RuntimeContext";
+import {Values} from "../models/Shared";
+import {WorkoutState} from "../models/Workouts";
+
+interface UseKpmReducerState {
+ lastStates: WorkoutState[]
+ kpm: number
+}
+
+export function useKpm() {
+ const {lastEvent} = useContext(RuntimeContext);
+
+ const [state, dispatch] = useReducer((state: UseKpmReducerState, newState: WorkoutState) => {
+ if (newState.time === 0) {
+ return {
+ lastStates: [],
+ kpm: 0,
+ };
+ }
+
+ const lastSeconds = Math.max(0, ...state.lastStates.map(s => s.time));
+ if (newState.time > lastSeconds) {
+ const sixtySecondsAgo = Math.max(0, newState.time - 60);
+ const inRange = state.lastStates.filter(s => s.time >= sixtySecondsAgo);
+ const first = inRange[0] || { time: 0, calories: 0 };
+ const duration = newState.time - first.time;
+
+ return {
+ lastStates: [...inRange, newState],
+ kpm: inRange.length > 30
+ ? Math.round(((newState.calories || 0) - (first.calories || 0)) * 60 / duration)
+ : 0,
+ }
+ }
+
+ return state;
+ }, {
+ lastStates: [],
+ kpm: 0,
+ });
+
+ useEffect(() => {
+ if (lastEvent?.workoutStates) {
+ for (const state of lastEvent.workoutStates) {
+ dispatch(state);
+ }
+ }
+ }, [lastEvent]);
+
+ return state.kpm;
+}
diff --git a/webui-react/src/pages/PlayPage.tsx b/webui-react/src/pages/PlayPage.tsx
index a5cec2c..044826c 100644
--- a/webui-react/src/pages/PlayPage.tsx
+++ b/webui-react/src/pages/PlayPage.tsx
@@ -19,6 +19,7 @@ import {ControlsBoi} from "./runtime/ControlsBoi";
import MessageBoi from "./runtime/MessageBoi";
import ProgramBoi from "./runtime/ProgramBoi";
import MilestoneBoi from "./runtime/MilestoneBoi";
+import {useKpm} from "../hooks/kpm";
function PlayPage(): JSX.Element {
const {active, ready, ended, workout, reset, resume} = useContext(RuntimeContext);
@@ -176,6 +177,7 @@ function CreatePlayPage(): JSX.Element {
function RunPlayPage(): JSX.Element {
const {workout} = useContext(RuntimeContext);
const lastState = useLastState();
+ const kpm = useKpm();
if (!workout || workout.status === WorkoutStatus.Created) {
return ;
@@ -193,6 +195,7 @@ function RunPlayPage(): JSX.Element {
+ {kpm > 0 && }
)}
diff --git a/webui-react/src/primitives/misc/Misc.tsx b/webui-react/src/primitives/misc/Misc.tsx
index b16dbd2..4b68d0f 100644
--- a/webui-react/src/primitives/misc/Misc.tsx
+++ b/webui-react/src/primitives/misc/Misc.tsx
@@ -11,7 +11,7 @@ export function TitleLine({children}: WithChildren) {
interface ValueProps {
raw: Values | number
- valueKey: keyof Values
+ valueKey: keyof Values | "kpm"
}
export function FluffyValue({raw, valueKey}: ValueProps): JSX.Element | null {
@@ -61,6 +61,10 @@ export function Value({raw, valueKey}: ValueProps): JSX.Element | null {
return <>{actual} rpm>
}
+ if (valueKey === "kpm") {
+ return <>{actual} kpm>
+ }
+
if (valueKey === "pulse") {
return (
<>