diff --git a/webui-react/src/contexts/RuntimeContext.tsx b/webui-react/src/contexts/RuntimeContext.tsx index f74c1df..05aa049 100644 --- a/webui-react/src/contexts/RuntimeContext.tsx +++ b/webui-react/src/contexts/RuntimeContext.tsx @@ -67,6 +67,7 @@ interface SocketOutput { workout: CurrentWorkout | null workoutStates: WorkoutState[] | null milestone: Milestone | null + oldMilestones: Milestone[] | null event: { name: string } | null error: { message: string } | null } @@ -132,8 +133,8 @@ export function RuntimeContextProvider({children}: WithChildren): JSX.Element { setEnded(false); setLastEvent(null); - const socket = runtimeRepo().openWebsocket(); - socket.onmessage = event => { + const newSocket = runtimeRepo().openWebsocket(); + newSocket.onmessage = event => { const data = socketOutput(event.data); setLastEvent(data); setReady(true); @@ -181,11 +182,11 @@ export function RuntimeContextProvider({children}: WithChildren): JSX.Element { } }; - socket.onclose = () => { + newSocket.onclose = () => { setEnded(true); }; - setSocket(socket); + setSocket(newSocket); }, []); const create = useCallback((options: CreateWorkoutOptions) => { diff --git a/webui-react/src/hooks/milestones.ts b/webui-react/src/hooks/milestones.ts index bfa2967..b2c85aa 100644 --- a/webui-react/src/hooks/milestones.ts +++ b/webui-react/src/hooks/milestones.ts @@ -3,7 +3,7 @@ import RuntimeContext from "../contexts/RuntimeContext"; import {Milestone} from "../models/Milestone"; import {ValueKey} from "../models/Shared"; -export function useCurrentMilestone(): Milestone | null { +export function useCurrentMilestone(showOld: boolean = false): Milestone | null { const {lastEvent} = useContext(RuntimeContext); const [milestone, setMilestone] = useState(null); @@ -12,7 +12,13 @@ export function useCurrentMilestone(): Milestone | null { if (lastEvent?.milestone) { setMilestone(lastEvent.milestone); } - }, [lastEvent]); + + if (showOld && lastEvent?.oldMilestones) { + for (const milestone of lastEvent.oldMilestones) { + setMilestone(milestone); + } + } + }, [showOld, lastEvent]); useEffect(() => { const handle = setTimeout(() => setMilestone(null), 1000); @@ -25,7 +31,7 @@ export function useCurrentMilestone(): Milestone | null { export function useLastMilestoneValue(valueKey: ValueKey, primaryKey: ValueKey) { const [value, setValue] = useState(0); - const current = useCurrentMilestone(); + const current = useCurrentMilestone(true); useEffect(() => { if (current && current.primaryKey === primaryKey) { diff --git a/webui-react/src/models/Shared.ts b/webui-react/src/models/Shared.ts index e1774f2..d6b2b60 100644 --- a/webui-react/src/models/Shared.ts +++ b/webui-react/src/models/Shared.ts @@ -17,6 +17,8 @@ export interface Values { calories?: number distance?: number level?: number + rpmSpeed?: number + pulse?: number } export type ColorName = "gray" | "green" | "blue" | "red" | "yellow" | "indigo"; diff --git a/webui-react/src/pages/PlayPage.tsx b/webui-react/src/pages/PlayPage.tsx index acdbc52..a5cec2c 100644 --- a/webui-react/src/pages/PlayPage.tsx +++ b/webui-react/src/pages/PlayPage.tsx @@ -8,7 +8,7 @@ import ProgramContext from "../contexts/ProgramContext"; import {Device} from "../models/Devices"; import {Program, subTitleOfProgram} from "../models/Programs"; import {useKey, usePlusMinus} from "../hooks/keyboard"; -import {TitleLine, Value} from "../primitives/misc/Misc"; +import {FluffyValue, TitleLine, Value} from "../primitives/misc/Misc"; import Blob, {BlobText, BlobTextLine} from "../primitives/blob/Blob"; import {Icon} from "../primitives/Shared"; import {faClose, faPlay} from "@fortawesome/free-solid-svg-icons"; @@ -27,13 +27,15 @@ function PlayPage(): JSX.Element { useEffect(() => { if (!active) { resume(); - } else if (active && ended) { - if (workout) { - navigate(`/workouts/${workout.id}`, {replace: true}); - reset(); - } } - }, [active, ready, ended, workout, resume]); + }, [active, resume]); + + useEffect(() => { + if (active && ended && workout) { + navigate(`/workouts/${workout.id}`, {replace: true}); + reset(); + } + }, [active, ready, ended, workout]); if (active && ready && workout === null) { return ; @@ -185,14 +187,13 @@ function RunPlayPage(): JSX.Element { {lastState && ( - + -
- -
- -
- + + + + +
)} {workout?.status === WorkoutStatus.Connected && } diff --git a/webui-react/src/pages/runtime/ControlsBoi.tsx b/webui-react/src/pages/runtime/ControlsBoi.tsx index fdb3ff7..24b8a65 100644 --- a/webui-react/src/pages/runtime/ControlsBoi.tsx +++ b/webui-react/src/pages/runtime/ControlsBoi.tsx @@ -23,7 +23,7 @@ export function ControlsBoi() { const {workout, disconnect, start, stop, setLevel, skip} = useContext(RuntimeContext); const lastState = useLastState(); - const [mode, setMode] = useState<"default" | "level" | "skip">("default"); + const [mode, setMode] = useState<"default" | "level" | "skip" | "disconnect">("default"); const [nextSkip, setNextSkip] = useState(-1); const [lastTime, setLastTime] = useState(0); @@ -50,7 +50,10 @@ export function ControlsBoi() { } if (isStopped) { - btnList.push({icon: faStop, onClick: disconnect}); + btnList.push({icon: faStop, onClick: () => { + disconnect(); + setMode("disconnect"); + }}); } return btnList; @@ -66,7 +69,7 @@ export function ControlsBoi() { if (mode !== "default") { if (mode === "level") { setLevel(sel + 1); - } else { + } else if (mode === "skip") { if (sel === 1) { setNextSkip(lastTime + 60 - (lastTime % 60)); } else if (sel === 2) { @@ -81,6 +84,8 @@ export function ControlsBoi() { } else { if (options[sel]) { options[sel].onClick(); + } else if (options.length > 0) { + options[0].onClick() } } @@ -102,7 +107,7 @@ export function ControlsBoi() { if (lastState?.level && mode === "level") { setSel(lastState.level - 1); } - }, [mode]); + }, [lastState, mode]); useEffect(() => { if (lastState?.time) { @@ -117,6 +122,12 @@ export function ControlsBoi() { } }, [lastTime, nextSkip]); + useEffect(() => { + if (mode === "default") { + setSel(0); + } + }, [mode]); + if (mode === "level") { return ; } @@ -125,6 +136,10 @@ export function ControlsBoi() { return ; } + if (mode === "disconnect") { + return + } + return ( {options.map((o, i) => ( diff --git a/webui-react/src/pages/runtime/MilestoneBoi.tsx b/webui-react/src/pages/runtime/MilestoneBoi.tsx index 5e4fd8c..cad6156 100644 --- a/webui-react/src/pages/runtime/MilestoneBoi.tsx +++ b/webui-react/src/pages/runtime/MilestoneBoi.tsx @@ -16,7 +16,7 @@ export default function MilestoneBoi() { const top = ; const bottom = []; for (const key in diff) { - bottom.push(
+
); + bottom.push(
+
); } return ( diff --git a/webui-react/src/pages/runtime/ProgramBoi.sass b/webui-react/src/pages/runtime/ProgramBoi.sass index 7fc6499..92783f0 100644 --- a/webui-react/src/pages/runtime/ProgramBoi.sass +++ b/webui-react/src/pages/runtime/ProgramBoi.sass @@ -12,14 +12,13 @@ overflow-x: hidden .HealthBar-entry-text - padding: 2px + padding: 0.1vmax font-size: 2.75vmax white-space: nowrap overflow-x: hidden &:first-child - padding: 4px - + padding: 0.2vmax .ProgressBar padding: 0.1vmax diff --git a/webui-react/src/pages/runtime/ProgramBoi.tsx b/webui-react/src/pages/runtime/ProgramBoi.tsx index ebe5abc..714ba69 100644 --- a/webui-react/src/pages/runtime/ProgramBoi.tsx +++ b/webui-react/src/pages/runtime/ProgramBoi.tsx @@ -213,7 +213,7 @@ function HealthBarProgress({progress}: ProgressProps) { return (
- {steps.map((step) => )} + {steps.map((step, i) => )}
); } diff --git a/webui-react/src/primitives/Shared.tsx b/webui-react/src/primitives/Shared.tsx index 73bfeb9..9e8f836 100644 --- a/webui-react/src/primitives/Shared.tsx +++ b/webui-react/src/primitives/Shared.tsx @@ -1,4 +1,4 @@ -import React, {CSSProperties, PropsWithChildren} from "react"; +import React, {CSSProperties, PropsWithChildren, useMemo} from "react"; import {IconProp} from "@fortawesome/fontawesome-svg-core"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -10,8 +10,13 @@ interface IconProps { value: IconProp spin?: boolean flash?: boolean + subtle?: boolean } -export function Icon({value, spin, flash}: IconProps) { - return ; +export function Icon({value, spin, flash, subtle}: IconProps) { + const style: CSSProperties = useMemo(() => { + return subtle ? {opacity: 0.5} : {}; + }, [subtle]); + + return ; } diff --git a/webui-react/src/primitives/misc/Misc.sass b/webui-react/src/primitives/misc/Misc.sass index 5bfc4e5..d87d252 100644 --- a/webui-react/src/primitives/misc/Misc.sass +++ b/webui-react/src/primitives/misc/Misc.sass @@ -8,3 +8,7 @@ padding-bottom: 0.1em color: $title-line border-bottom: $blob-background 1px solid + +.FluffyValue + display: block + min-height: 4vmax diff --git a/webui-react/src/primitives/misc/Misc.tsx b/webui-react/src/primitives/misc/Misc.tsx index 0d30d58..b16dbd2 100644 --- a/webui-react/src/primitives/misc/Misc.tsx +++ b/webui-react/src/primitives/misc/Misc.tsx @@ -1,6 +1,7 @@ import "./Misc.sass"; -import {WithChildren} from "../Shared"; +import {Icon, WithChildren} from "../Shared"; import {Values} from "../../models/Shared"; +import {faArrowUpRightDots, faHeart} from "@fortawesome/free-solid-svg-icons"; export function TitleLine({children}: WithChildren) { return ( @@ -13,6 +14,18 @@ interface ValueProps { valueKey: keyof Values } +export function FluffyValue({raw, valueKey}: ValueProps): JSX.Element | null { + if (typeof raw !== "number" && (raw[valueKey] === undefined || raw[valueKey] === null)) { + return null; + } + + return ( +
+ +
+ ); +} + export function Value({raw, valueKey}: ValueProps): JSX.Element | null { const actual = typeof raw === "number" ? raw : (raw[valueKey]); @@ -35,7 +48,27 @@ export function Value({raw, valueKey}: ValueProps): JSX.Element | null { } if (valueKey === "level") { - return <>{actual} lvl; + return ( + <> + + {" "} + {actual} + + ); + } + + if (valueKey === "rpmSpeed") { + return <>{actual} rpm + } + + if (valueKey === "pulse") { + return ( + <> + + {" "} + {actual} + + ); } } diff --git a/ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/models/Milestones.kt b/ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/models/Milestones.kt new file mode 100644 index 0000000..dea11a6 --- /dev/null +++ b/ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/models/Milestones.kt @@ -0,0 +1,89 @@ +package net.aiterp.git.ykonsole2.domain.models + +import net.aiterp.git.ykonsole2.domain.runtime.* + +fun MutableMap>.tryMilestone(event: ValuesReceived): MilestoneReached? = tryMilestone(event.values) + +fun List.makeMilestoneReachedEvents(): List { + val cache = mutableMapOf>() + + return mapNotNull { cache.tryMilestone(it) } +} + +private fun MutableMap>.tryMilestone(state: WorkoutState): MilestoneReached? = + tryMilestone(state.asValueList()) + +private fun MutableMap>.tryMilestone(newValues: List): MilestoneReached? { + val time = newValues.find