diff --git a/README.md b/README.md index 201c342..38bff61 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/app.tsx b/src/app.tsx index 1671511..a237367 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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, diff --git a/src/hooks/use-gear-control.ts b/src/hooks/use-gear-control.ts index 397252d..1acf7d8 100644 --- a/src/hooks/use-gear-control.ts +++ b/src/hooks/use-gear-control.ts @@ -31,6 +31,7 @@ export function useGearControl({ const [shiftFlash, setShiftFlash] = useState(); const gearRef = useRef(gear); const keyboardControlsEnabled = useRef(true); + const sessionOpen = useRef(false); const shiftFlashTimer = useRef(undefined); const maximumGearRef = useRef(maximumGear); @@ -87,7 +88,7 @@ export function useGearControl({ if ( event.defaultPrevented || keyboardEventHasModifiers(event) || - keyboardEventUsesNativeEnterAction(event) || + (!sessionOpen.current && keyboardEventUsesNativeEnterAction(event)) || !keyboardControlsEnabled.current || (!isGearControl && eventTargetsEditableControl(event)) ) { @@ -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 }; diff --git a/src/hooks/use-trainer.ts b/src/hooks/use-trainer.ts index 4d0154b..0b21a20 100644 --- a/src/hooks/use-trainer.ts +++ b/src/hooks/use-trainer.ts @@ -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, @@ -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 @@ -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) => {