feat(web): hold a Screen Wake Lock while reading or listening
Release / build-and-push (push) Successful in 3m12s

New wake-lock.ts engine (sanctioned island, dependency-injected for
tests): feature-detected navigator.wakeLock, silent no-op when
unsupported or denied, and the spec-mandated force-release-on-hide →
re-acquire-on-visible loop handled via the sentinel release event.
The player acquires on play and releases on pause (sleep-timer fades
ride the same path); the reader holds the lock for the session. Both
gate on the keep_screen_awake preference — data-keep-awake on the
player root, keepScreenAwake in readerInitExpr.

Hidden-tab playback intentionally does not keep the screen awake:
the spec forbids screen wake locks for hidden documents.
This commit is contained in:
John O'Keefe committed 2026-10-26 13:59:37 -04:00
1 parent c645b8e207
commit afbf1029a9
8 files changed
+375 -56

No files matched your search

+11
View File
@@ -15,6 +15,7 @@
import { Alpine } from "./alpine";
import { renderNoteHtml } from "./markdown";
import { showToast } from "./toast";
import { WakeLockManager } from "./wake-lock";
import {
MOUSE_GUIDE,
resolveShortcut,
@@ -288,6 +289,13 @@ Alpine.data("audioReader", () => ({
// 30s of a countdown. Reset by stop/finish.
sleepDim: 0,
// Screen wake lock (keep_screen_awake profile preference): held while
// playing so the display doesn't blank/lock mid-listen. The browser
// force-releases when the page hides; the manager re-acquires on
// visibility regain while audio is still going.
wakeLock: new WakeLockManager(),
keepAwakeEnabled: false,
// Settings popover (plan §4): header gear → panel. Autoplay-on-open
// is the first option; the panel is structured for more.
settingsOpen: false,
@@ -336,6 +344,7 @@ Alpine.data("audioReader", () => ({
const title = body.getAttribute("data-title") || "";
const cover = body.getAttribute("data-cover") || "";
const fileUrl = body.getAttribute("data-file-url") || "";
this.keepAwakeEnabled = body.getAttribute("data-keep-awake") === "true";
this.mediaItemId = mediaId;
this.title = title;
this.fileUrl = fileUrl;
@@ -413,6 +422,7 @@ Alpine.data("audioReader", () => ({
// is awake and asked for audio); stopSleep restores the volume.
if (this.sleep.mode === "fading") this.stopSleep();
this.updateMediaSessionState();
if (this.keepAwakeEnabled) void this.wakeLock.acquire();
});
audio.addEventListener("pause", () => {
const now = performance.now();
@@ -423,6 +433,7 @@ Alpine.data("audioReader", () => ({
this.playing = false;
this.pushProgress(true);
this.updateMediaSessionState();
if (this.keepAwakeEnabled) this.wakeLock.release();
});
window.addEventListener("pagehide", () => this.pushProgress(true));
window.addEventListener("resize", () => this.recomputeTicks());