services/screensaver.jsxjavascript
/*
Combines idle detection (useIdle) with the configured timeout/fade values
(useConfig) into a single ScreensaverContext. Waking from idle also resets
the kiosk session: the app re-navigates to the same route (pathname+search,
not back to "/") while the screensaver is still fading out, so the next
visitor starts that page fresh instead of inheriting the previous visitor's
open menus and selections - re-navigating remounts the page component,
which is what clears that per-page state, no explicit reset needed.
Previously this went back to the app-selection screen ("/") instead; that
was changed to stay on the current page/app so a visitor's search params
and place in the exhibit survive a screensaver interruption (see
"refactor: Refine navigation flow and screensaver exit behavior"). The
reset deliberately happens on wake-up rather than when going idle: the
screensaver video is defined by the current route's content file (see
Screensaver.jsx), and navigating away at idle start would unload it
mid-show.
Must be mounted inside the Router (see src/HashRouter.jsx): useConfig (via
useSearchParams) requires router context, and the session reset needs
useNavigate/useLocation. Same reason ContentProvider now lives in
HashRouter.jsx instead of App.jsx - TabProvider stays in App.jsx since it
doesn't use useConfig.
*/
// allow export of contexts and hooks - shouldn't be flagged as non-component
/* eslint react-refresh/only-export-components: 0 */
import { createContext, useContext, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useNavigate, useLocation } from "react-router";
import { useConfig } from "./config/config";
import { useIdle } from "../hooks/idle";
const ScreensaverContext = createContext();
/**
* Tracks idle state and exposes it via ScreensaverContext.
*/
export const ScreensaverProvider = ({ children }) => {
const { screensaverTimeout, screensaverFadeDuration } = useConfig();
// screensaverTimeout is in seconds (see config.jsx), useIdle expects ms.
// console.log("dont forget to return timeout to normal", screensaverTimeout);
const { isIdle, trigger, forceIdle } = useIdle(screensaverTimeout * 1000);
const navigate = useNavigate();
const { pathname, search } = useLocation();
// session reset on the idle -> active transition only (not on mount, not
// while idle): happens behind the still-opaque fading screensaver, no view
// transition needed; keeps the kiosk config params (config-schema.js) alive
const wasIdleRef = useRef(false);
useEffect(() => {
if (wasIdleRef.current && !isIdle && pathname !== "/") navigate(`${pathname}${search}`);
wasIdleRef.current = isIdle;
}, [isIdle, pathname, search, navigate]);
return (
<ScreensaverContext.Provider
value={{ isIdle, trigger, screensaverFadeDuration, showNow: forceIdle }}>
{children}
</ScreensaverContext.Provider>
);
};
ScreensaverProvider.propTypes = {
children: PropTypes.node.isRequired,
};
/**
* React hook to access { isIdle, trigger, screensaverFadeDuration, showNow }.
* Must be used within a `<ScreensaverProvider>`.
*/
export const useScreensaver = () => {
return useContext(ScreensaverContext);
};