From d2e5108d3f32eca532e350b2d1c5cb34279ba2cf Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Sun, 8 Feb 2026 21:05:26 +0100 Subject: [PATCH] Prevent screen dimming during playback --- CHANGELOG.md | 1 + src/App.tsx | 32 +++++++++++++++++++++++++++++++ src/lib/wakelock.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/lib/wakelock.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 488a5ad..a747310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add `Shift+↑/↓` shortcuts for adjusting WPM by 100 and document them in the shortcuts modal - Improve mobile support for long words by shifting the focal point left - Fix major performance issues on long texts by reducing frequent localStorage writes during playback and keeping global keyboard listeners stable +- Keep the screen awake while playing when the browser supports the Screen Wake Lock API ### 2026-02-07: 1.1.0 diff --git a/src/App.tsx b/src/App.tsx index 985f053..872f7fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,7 @@ import { saveTextPayload, setPositionForTextInPlace, } from "./lib/storage"; +import { createWakeLockHandle } from "./lib/wakelock"; import { styles } from "./styles"; import type { BookMetadata, PositionsMap } from "./types"; @@ -119,16 +120,22 @@ export default function App() { const lastIndexRef = useRef(currentIndex); const currentIndexRef = useRef(currentIndex); const wordsLengthRef = useRef(words.length); + const isPlayingRef = useRef(isPlaying); const speedScaleRef = useRef(1); const observedWpmRef = useRef(null); const observedWpmEmaRef = useRef(null); const lastControlTickRef = useRef(null); const wasPlayingRef = useRef(false); + const wakeLockRef = useRef(createWakeLockHandle()); useEffect(() => { currentIndexRef.current = currentIndex; }, [currentIndex]); + useEffect(() => { + isPlayingRef.current = isPlaying; + }, [isPlaying]); + useEffect(() => { wordsLengthRef.current = words.length; }, [words.length]); @@ -604,6 +611,31 @@ export default function App() { document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); + // Keep the screen awake while playing (best effort; may be unsupported). + useEffect(() => { + const wl = wakeLockRef.current; + if (!isPlaying) { + void wl.release(); + return; + } + + void wl.request(); + + const onVisibilityChange = () => { + if (document.visibilityState === "visible" && isPlayingRef.current) { + void wl.request(); + } else { + void wl.release(); + } + }; + + document.addEventListener("visibilitychange", onVisibilityChange); + return () => { + document.removeEventListener("visibilitychange", onVisibilityChange); + void wl.release(); + }; + }, [isPlaying]); + const handleProgressClick = useCallback( (e: ReactMouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); diff --git a/src/lib/wakelock.ts b/src/lib/wakelock.ts new file mode 100644 index 0000000..6954c65 --- /dev/null +++ b/src/lib/wakelock.ts @@ -0,0 +1,46 @@ +// Best-effort screen wake lock helper. +// Supported in Chromium-based browsers and some others; not supported in iOS Safari. + +export type WakeLockHandle = { + request: () => Promise; + release: () => Promise; +}; + +type WakeLockSentinelLike = { + released: boolean; + release: () => Promise; +}; + +export function createWakeLockHandle(): WakeLockHandle { + let sentinel: WakeLockSentinelLike | null = null; + + const request = async () => { + // Re-request is allowed; only keep one sentinel. + if (sentinel && !sentinel.released) return; + + const wl = (navigator as unknown as { wakeLock?: { request: (type: "screen") => Promise } }) + .wakeLock; + if (!wl) return; + + try { + sentinel = await wl.request("screen"); + } catch { + // Not allowed (e.g. user gesture required, tab not visible) or unsupported. + sentinel = null; + } + }; + + const release = async () => { + const s = sentinel; + sentinel = null; + if (!s || s.released) return; + try { + await s.release(); + } catch { + // Ignore. + } + }; + + return { request, release }; +} +