Files
EventSnap/frontend/src/lib/diashow/wakelock.ts
MechaCat02 4026648f98 fix(diashow): working transitions + slide/push, fullscreen, activity controls
Transitions never actually animated: Svelte scopes @keyframes names but does NOT
rewrite animation references in inline style attributes, so the inline
'animation: crossfade-in ...' never matched its scoped keyframe — a hard cut, no fade
(Ken Burns' zoom was dead too). Move the animation into scoped classes and pass dynamic
values via CSS custom properties.

- Real two-layer crossfade: hold the outgoing frame opaque beneath the incoming one
  (which is decode-gated), so there's no black flash and no decode pop.
- New slide transitions (from below/left/right) as one reusable component; the previous
  frame is pushed off the opposite edge in step (a conveyor/push), easeInOutSine 1s.
- Fullscreen toggle button (Fullscreen API) + 'f' shortcut; Esc in fullscreen no longer
  also navigates away.
- Controls now reveal on pointer/keyboard activity and fade when idle (cursor hides too)
  instead of being always visible.
- wakelock: null the sentinel on the OS 'release' event so re-acquire-on-visible
  actually fires (previously the screen could sleep after the tab was first hidden).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:22:15 +02:00

67 lines
2.2 KiB
TypeScript

// Thin wrapper around the Screen Wake Lock API. Held by the diashow page so a phone
// driving a projector doesn't sleep. No-op on unsupported browsers (Firefox, older
// Safari).
//
// Wake locks die when the document goes hidden; the page re-acquires on visible to
// keep the screen on across short interruptions.
interface SentinelLike {
release: () => Promise<void>;
// WakeLockSentinel is an EventTarget; it fires `release` when the lock ends — including
// when the OS drops it because the tab was hidden.
addEventListener?: (type: 'release', listener: () => void) => void;
}
type WakeLock = { request: (t: string) => Promise<SentinelLike> };
let sentinel: SentinelLike | null = null;
let visibilityHandler: (() => void) | null = null;
async function request(wakeLock: WakeLock): Promise<void> {
try {
sentinel = await wakeLock.request('screen');
// The OS auto-releases the lock when the tab is hidden. Without clearing our handle
// on that event, the visibility handler's `sentinel === null` guard would never be
// true and the lock would never be re-acquired — the screen could then sleep after
// the first time the tab lost focus. Null it on release so re-acquire can fire.
sentinel.addEventListener?.('release', () => {
sentinel = null;
});
} catch {
// User denied, or already released — nothing useful to do.
sentinel = null;
}
}
export async function acquireWakeLock(): Promise<void> {
const wakeLock = (navigator as Navigator & { wakeLock?: WakeLock }).wakeLock;
if (!wakeLock) return;
await request(wakeLock);
// Re-acquire when the page becomes visible again (the OS releases the lock
// while the tab is hidden).
if (!visibilityHandler) {
visibilityHandler = () => {
if (document.visibilityState === 'visible' && sentinel === null) {
void request(wakeLock);
}
};
document.addEventListener('visibilitychange', visibilityHandler);
}
}
export async function releaseWakeLock(): Promise<void> {
if (sentinel) {
try {
await sentinel.release();
} catch {
// ignore — release after release is fine
}
sentinel = null;
}
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler);
visibilityHandler = null;
}
}