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>
67 lines
2.2 KiB
TypeScript
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;
|
|
}
|
|
}
|