Stian Fredrik Aune
2 years ago
20 changed files with 287 additions and 40 deletions
-
3webui-react/src/contexts/RuntimeContext.tsx
-
37webui-react/src/hooks/milestones.ts
-
8webui-react/src/models/Milestone.ts
-
1webui-react/src/models/Shared.ts
-
2webui-react/src/pages/PlayPage.tsx
-
5webui-react/src/pages/runtime/MilestoneBoi.sass
-
35webui-react/src/pages/runtime/MilestoneBoi.tsx
-
18webui-react/src/pages/runtime/ProgramBoi.sass
-
77webui-react/src/pages/runtime/ProgramBoi.tsx
-
3webui-react/src/primitives/misc/Misc.tsx
-
5ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/models/WorkoutState.kt
-
1ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/runtime/Event.kt
-
83ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/MilestoneChecker.kt
-
4ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/WorkoutWriter.kt
-
14ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/abstracts/PassiveDriver.kt
-
7ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/testing/TestDriver.kt
-
16ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/Shared.kt
-
3ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/ws/Connection.kt
-
2ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/ws/SocketOutput.kt
-
3ykonsole-server/src/main/kotlin/net/aiterp/git/ykonsole2/Server.kt
@ -0,0 +1,37 @@ |
|||
import {useContext, useEffect, useMemo, useReducer, useState} from "react"; |
|||
import RuntimeContext from "../contexts/RuntimeContext"; |
|||
import {Milestone} from "../models/Milestone"; |
|||
import {ValueKey} from "../models/Shared"; |
|||
|
|||
export function useCurrentMilestone(): Milestone | null { |
|||
const {lastEvent} = useContext(RuntimeContext); |
|||
|
|||
const [milestone, setMilestone] = useState<Milestone | null>(null); |
|||
|
|||
useEffect(() => { |
|||
if (lastEvent?.milestone) { |
|||
setMilestone(lastEvent.milestone); |
|||
} |
|||
}, [lastEvent]); |
|||
|
|||
useEffect(() => { |
|||
const handle = setTimeout(() => setMilestone(null), 1000); |
|||
|
|||
return () => clearTimeout(handle); |
|||
}, [milestone]); |
|||
|
|||
return milestone; |
|||
} |
|||
|
|||
export function useLastMilestoneValue(valueKey: ValueKey, primaryKey: ValueKey) { |
|||
const [value, setValue] = useState<number>(0); |
|||
const current = useCurrentMilestone(); |
|||
|
|||
useEffect(() => { |
|||
if (current && current.primaryKey === primaryKey) { |
|||
setValue(prev => current.current[valueKey] || prev); |
|||
} |
|||
}, [current, primaryKey, valueKey]); |
|||
|
|||
return value; |
|||
} |
@ -0,0 +1,8 @@ |
|||
import {ValueKey, Values} from "./Shared"; |
|||
|
|||
export interface Milestone<K extends ValueKey = ValueKey> { |
|||
primary: Values & { [P in K]: number } |
|||
primaryKey: K |
|||
current: Values |
|||
diff: Values |
|||
} |
@ -0,0 +1,5 @@ |
|||
.MilestoneBoi-inner |
|||
margin: 0.25em 0.5ch |
|||
|
|||
.MilestoneBoi-top |
|||
font-size: 150% |
@ -0,0 +1,35 @@ |
|||
import "./MilestoneBoi.sass"; |
|||
import {useCurrentMilestone} from "../../hooks/milestones"; |
|||
import {Boi} from "../../primitives/boi/Boi"; |
|||
import {Value} from "../../primitives/misc/Misc"; |
|||
import {ValueKey} from "../../models/Shared"; |
|||
|
|||
export default function MilestoneBoi() { |
|||
const milestone = useCurrentMilestone(); |
|||
if (milestone === null) { |
|||
return null; |
|||
} |
|||
|
|||
const {primary, primaryKey, diff} = milestone; |
|||
|
|||
|
|||
const top = <Value raw={primary} valueKey={primaryKey}/>; |
|||
const bottom = []; |
|||
for (const key in diff) { |
|||
bottom.push(<div>+<Value raw={diff} valueKey={(key) as ValueKey}/></div>); |
|||
} |
|||
|
|||
return ( |
|||
<Boi vertical="center" horizontal="center"> |
|||
<div className="MilestoneBoi-inner"> |
|||
<span className="MilestoneBoi-top"> |
|||
{top} |
|||
</span> |
|||
<br/> |
|||
<span className="MilestoneBoi-bottom"> |
|||
{bottom} |
|||
</span> |
|||
</div> |
|||
</Boi> |
|||
); |
|||
} |
@ -0,0 +1,83 @@ |
|||
package net.aiterp.git.ykonsole2.infrastructure.drivers |
|||
|
|||
import net.aiterp.git.ykonsole2.domain.runtime.* |
|||
import net.aiterp.git.ykonsole2.infrastructure.drivers.abstracts.PassiveDriver |
|||
import java.util.concurrent.ConcurrentHashMap |
|||
|
|||
class MilestoneChecker : PassiveDriver() { |
|||
private val milestoneMap = ConcurrentHashMap<Value, List<Value>>() |
|||
|
|||
override suspend fun onEvent(event: Event, output: FlowBus<Event>) { |
|||
if (event is Connected || event is Disconnected) { |
|||
milestoneMap.clear() |
|||
} |
|||
|
|||
if (event is ValuesReceived) { |
|||
val time = event.values.find<Time>() ?: return |
|||
val calories = event.values.find<Calories>() ?: return |
|||
val distance = event.values.find<Distance>() ?: return |
|||
|
|||
var next: MilestoneReached? = null |
|||
|
|||
val roundDistance = distance.roundedDown |
|||
if (roundDistance.isMilestone && !milestoneMap.containsKey(roundDistance)) { |
|||
val last = milestoneMap.getOrDefault(distance.lastMilestone, default) |
|||
|
|||
milestoneMap[roundDistance] = listOf(time, calories) |
|||
next = MilestoneReached(roundDistance, listOf(time, calories), listOf( |
|||
Time(time.seconds - last.findInt<Time>()), |
|||
Calories(calories.kcal - last.findInt<Calories>()), |
|||
)) |
|||
} |
|||
|
|||
val roundedCals = calories.roundedDown |
|||
if (roundedCals.isMilestone && !milestoneMap.containsKey(roundedCals)) { |
|||
val last = milestoneMap.getOrDefault(roundedCals.lastMilestone, default) |
|||
|
|||
milestoneMap[roundedCals] = listOf(time, distance) |
|||
next = MilestoneReached(roundedCals, listOf(time, distance), listOf( |
|||
Time(time.seconds - last.findInt<Time>()), |
|||
Distance(distance.meters - last.findInt<Distance>()), |
|||
)) |
|||
} |
|||
|
|||
val roundedTime = time.roundedDown |
|||
if (roundedTime.isMilestone && !milestoneMap.containsKey(roundedTime)) { |
|||
val last = milestoneMap.getOrDefault(time.lastMilestone, default) |
|||
|
|||
milestoneMap[roundedTime] = listOf(calories, distance) |
|||
next = MilestoneReached(roundedTime, listOf(calories, distance), listOf( |
|||
Calories(calories.kcal - last.findInt<Calories>()), |
|||
Distance(distance.meters - last.findInt<Distance>()), |
|||
)) |
|||
} |
|||
|
|||
next?.let { output.emit(it) } |
|||
} |
|||
} |
|||
|
|||
companion object { |
|||
private val default = listOf(Time(0), Calories(0), Distance(0)) |
|||
|
|||
private val Value.roundedDown get() = when (this) { |
|||
is Time -> Time(seconds - (seconds % 2)) |
|||
is Calories -> Calories(kcal - (kcal % 4)) |
|||
is Distance -> Distance(meters - (meters % 10)) |
|||
else -> error("Not milestone-able") |
|||
} |
|||
|
|||
private val Value.isMilestone get() = when (this) { |
|||
is Time -> seconds > 0 && seconds % 60 == 0 |
|||
is Calories -> kcal > 0 && kcal % 100 == 0 |
|||
is Distance -> meters > 0 && meters % 1000 == 0 |
|||
else -> error("Not milestone-able") |
|||
} |
|||
|
|||
private val Value.lastMilestone get() = when (this) { |
|||
is Time -> Time(maxOf(0, (seconds - 1) - ((seconds - 1) % 60))) |
|||
is Calories -> Calories(maxOf(0, (kcal - 1) - ((kcal - 1) % 100))) |
|||
is Distance -> Distance(maxOf(0, (meters - 1) - ((meters - 1) % 1000))) |
|||
else -> error("Not milestone-able") |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
package net.aiterp.git.ykonsole2.infrastructure.drivers.abstracts |
|||
|
|||
import net.aiterp.git.ykonsole2.domain.runtime.Command |
|||
import net.aiterp.git.ykonsole2.domain.runtime.Driver |
|||
import net.aiterp.git.ykonsole2.domain.runtime.Event |
|||
import net.aiterp.git.ykonsole2.domain.runtime.FlowBus |
|||
|
|||
abstract class PassiveDriver : Driver { |
|||
protected abstract suspend fun onEvent(event: Event, output: FlowBus<Event>) |
|||
|
|||
override suspend fun start(input: FlowBus<Command>, output: FlowBus<Event>) { |
|||
output.collect { onEvent(it, output) } |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue