Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Bike trainer control web app using Web Bluetooth. Tested with Wahoo KICKR Core 2
- Continues any saved session in a new unsaved copy while preserving its recorded time, distance, calories, samples, averages, maximums, and original start time. Linked course sessions expose compact part-number navigation through the continuation path, plus an all-parts view that combines every session on that path without mixing in alternate branches. Active rides are checkpointed locally and restored after a page reload; the restored dashboard explains that ride data remains safe while Bluetooth devices may need time to reconnect before riding continues, then automatically removes that notice once the trainer and every other paired ride device are connected again.
- Protects recorded active rides with a browser confirmation before refresh or close, and presents the save workflow before starting or continuing another session.
- Includes contextual keyboard help for dashboard and history actions, including pausing, ending, starting, navigating, viewing history, and deleting sessions.
- Reserves Return for shifting up on the dashboard while a session is open, including its initial auto-pause and manual pauses; in direct-resistance mode it increases resistance instead. Focused buttons and links cannot activate from Return during that time, even when the trainer is disconnected or the highest gear is reached. Right Shift still shifts down. Dialogs and editable fields retain their normal keyboard behavior, and Return can activate **Start new session** after a ride has ended.
- Displays connection and application notices with a visible 15-second countdown and automatic dismissal.
- Keeps ride and session data local to the current browser profile; no account is required. Enhanced GPX processing uploads a selected route only after explicit approval, processes it ephemerally in the Ride Control Worker, and does not store the source file. Choosing on-device GPX processing keeps the complete file and its coordinates on that device. Other import paths may send only the first coordinate of a route without a description to OpenStreetMap's Nominatim service to find a starting-city label; that result is cached in the browser and stored with the workout. The resulting location sentence links to OpenStreetMap in a new tab, frames the complete route area, and marks the starting coordinate.

Expand Down
11 changes: 9 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -633,12 +633,19 @@ export function App({ initialSession = emptySession }: { initialSession?: Stored
}, [warnBeforeUnload]);

useEffect(() => {
trainer.setKeyboardControlsEnabled(dashboardKeyboardEnabled && !dashboardWorkout.workout);
trainer.setKeyboardControlsEnabled(
dashboardKeyboardEnabled && !dashboardWorkout.workout,
!session.ended
);
trainer.setGearControlsEnabled(virtualShiftingActive);
gearControl.setKeyboardControlsEnabled(dashboardKeyboardEnabled && virtualShiftingActive);
gearControl.setKeyboardControlsEnabled(
dashboardKeyboardEnabled && virtualShiftingActive,
!session.ended
);
}, [
dashboardKeyboardEnabled,
gearControl.setKeyboardControlsEnabled,
session.ended,
trainer.setGearControlsEnabled,
trainer.setKeyboardControlsEnabled,
dashboardWorkout.workout,
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/use-gear-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function useGearControl({
const [shiftFlash, setShiftFlash] = useState<ResistanceAdjustmentDirection | undefined>();
const gearRef = useRef(gear);
const keyboardControlsEnabled = useRef(true);
const sessionOpen = useRef(false);
const shiftFlashTimer = useRef<number | undefined>(undefined);
const maximumGearRef = useRef(maximumGear);

Expand Down Expand Up @@ -87,7 +88,7 @@ export function useGearControl({
if (
event.defaultPrevented ||
keyboardEventHasModifiers(event) ||
keyboardEventUsesNativeEnterAction(event) ||
(!sessionOpen.current && keyboardEventUsesNativeEnterAction(event)) ||
!keyboardControlsEnabled.current ||
(!isGearControl && eventTargetsEditableControl(event))
) {
Expand All @@ -113,8 +114,9 @@ export function useGearControl({
[]
);

const setKeyboardControlsEnabled = useCallback((enabled: boolean) => {
const setKeyboardControlsEnabled = useCallback((enabled: boolean, open: boolean) => {
keyboardControlsEnabled.current = enabled;
sessionOpen.current = open;
}, []);

return { gear, setKeyboardControlsEnabled, shiftFlash, shiftGear };
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/use-trainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function useTrainer(
const rememberedResistance = useRef(store.get().resistance);
const resistanceTarget = useRef(store.get().resistance);
const keyboardControlsEnabled = useRef(true);
const sessionOpen = useRef(false);
const gearControlsEnabled = useRef(false);
const trainerConnection = useTrainerConnection(
store,
Expand Down Expand Up @@ -207,7 +208,7 @@ export function useTrainer(
if (
event.defaultPrevented ||
keyboardEventHasModifiers(event) ||
keyboardEventUsesNativeEnterAction(event) ||
(!sessionOpen.current && keyboardEventUsesNativeEnterAction(event)) ||
(!isResistanceControl && eventTargetsEditableControl(event)) ||
!keyboardControlsEnabled.current ||
gearControlsEnabled.current
Expand Down Expand Up @@ -247,8 +248,9 @@ export function useTrainer(
};
}, [setResistanceKeyFlash, updateResistance]);

const setKeyboardControlsEnabled = useCallback((enabled: boolean) => {
const setKeyboardControlsEnabled = useCallback((enabled: boolean, open: boolean) => {
keyboardControlsEnabled.current = enabled;
sessionOpen.current = open;
}, []);

const setGearControlsEnabled = useCallback((enabled: boolean) => {
Expand Down