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
112 changes: 112 additions & 0 deletions client/src/hooks/use-wake-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useCallback, useEffect, useRef, useState } from "react";

/**
* A custom hook to manage a screen wake lock.
*
* @returns An object with an `isLocked` boolean and `request`/`release` functions.
*/
Comment thread
Copilot marked this conversation as resolved.
export const useWakeLock = () => {
const wakeLock = useRef<WakeLockSentinel | null>(null);
const shouldHoldLock = useRef(false);
const requestInFlight = useRef(false);
const wakeLockSupported = useRef<boolean | null>(null);
const [isLocked, setIsLocked] = useState(false);
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.

const request = useCallback(async () => {
if (wakeLockSupported.current === false) {
return;
}

if (wakeLockSupported.current === null) {
wakeLockSupported.current = "wakeLock" in navigator;
if (!wakeLockSupported.current) {
console.warn("Screen Wake Lock API not supported");
return;
}
}

shouldHoldLock.current = true;

if (wakeLock.current || requestInFlight.current) {
return;
}
Comment thread
Copilot marked this conversation as resolved.

requestInFlight.current = true;

try {
const sentinel = await navigator.wakeLock.request("screen");

// If release() was called while the request was in flight,
// release the new sentinel immediately and do nothing.
if (!shouldHoldLock.current) {
void sentinel.release();
return;
}
Comment on lines +28 to +44

wakeLock.current = sentinel;
setIsLocked(true);

sentinel.addEventListener(
"release",
() => {
if (wakeLock.current === sentinel) {
wakeLock.current = null;
setIsLocked(false);
}
},
{ once: true }
);
} catch (err) {
console.error("Wake Lock request failed:", err);
setIsLocked(false);
} finally {
requestInFlight.current = false;
}
Comment thread
Copilot marked this conversation as resolved.
}, []);
Comment thread
Copilot marked this conversation as resolved.

const release = useCallback(async () => {
shouldHoldLock.current = false;

if (!wakeLock.current) {
setIsLocked(false);
return;
}

try {
await wakeLock.current.release();
} catch {
// ignore (it may have already been released by the browser)
} finally {
wakeLock.current = null;
setIsLocked(false);
}
}, []);
Comment thread
Copilot marked this conversation as resolved.

useEffect(() => {
const handleVisibilityChange = () => {
if (
shouldHoldLock.current &&
!isLocked &&
document.visibilityState === "visible"
) {
void request();
}
};

document.addEventListener("visibilitychange", handleVisibilityChange);
document.addEventListener("fullscreenchange", handleVisibilityChange);

return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
document.removeEventListener("fullscreenchange", handleVisibilityChange);

shouldHoldLock.current = false;
const sentinel = wakeLock.current;
wakeLock.current = null;
requestInFlight.current = false;
void sentinel?.release();
};
Comment thread
Copilot marked this conversation as resolved.
Comment on lines +103 to +108
}, [request, release, isLocked]);

return { isLocked, request, release };
};
17 changes: 17 additions & 0 deletions client/src/pages/tools/timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "@/lib/time-tools";
import { getParam, updateURL, copyShareableURL } from "@/lib/url-sharing";
import { useToast } from "@/hooks/use-toast";
import { useWakeLock } from "@/hooks/use-wake-lock";
import {
ToolButton,
ResetButton,
Expand Down Expand Up @@ -68,6 +69,22 @@ export default function Timer() {
const audioContextRef = useRef<AudioContext | null>(null);
const alarmIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
const { toast } = useToast();
const {
isLocked,
request: requestWakeLock,
release: releaseWakeLock,
} = useWakeLock();

// Effect to manage the wake lock
useEffect(() => {
const hasRunningTimers = timers.some(timer => timer.isRunning);

if (hasRunningTimers) {
void requestWakeLock();
} else {
void releaseWakeLock();
}
}, [timers, isLocked, requestWakeLock, releaseWakeLock]);
Comment thread
Copilot marked this conversation as resolved.
Comment on lines +78 to +87

// Clear any alarm handle (interval or timeout) robustly
const clearAlarmHandle = useCallback((handle: NodeJS.Timeout) => {
Expand Down
Loading