Files
eranos/src/hooks/usePlayerControls.ts
T
Alex Gleason 7f37f16c7b Upgrade React from 18.3 to 19.2
- Upgrade react, react-dom to ^19.2.4 and @types/react, @types/react-dom to v19
- Upgrade @nostrify/react to 0.4.0 (peer deps fix for React 19)
- Upgrade vaul to 1.1.2 and react-day-picker to 9.14.0 for React 19 compatibility
- Fix useRef() calls to pass explicit initial values (required in React 19)
- Update RefObject types to include null (React 19 type change)
- Rewrite Calendar component for react-day-picker v9 classNames API
- Add npm overrides for @emoji-mart/react (only remaining React 18 holdout)
2026-03-28 00:29:37 -05:00

116 lines
3.9 KiB
TypeScript

import { useRef, useState, useEffect, useCallback, RefObject } from 'react';
interface UsePlayerControlsOptions {
/** The media element (audio or video) to observe and pause when offscreen. */
mediaRef: RefObject<HTMLMediaElement | null>;
/** The container element to observe for IntersectionObserver and mouse events. */
containerRef: RefObject<HTMLElement | null>;
/** Whether the media is currently playing. */
isPlaying: boolean;
}
interface UsePlayerControlsReturn {
showControls: boolean;
revealControls: () => void;
scheduleHide: () => void;
isMuted: boolean;
volume: number;
toggleMute: (e: React.MouseEvent) => void;
handleVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
/**
* Shared player control behaviour used by both VideoPlayer and AudioVisualizer:
* - Auto-hides controls 2.5 s after the last mouse movement while playing.
* - Pauses playback when the container scrolls out of view.
* - Manages volume state and mute toggling.
*/
export function usePlayerControls({
mediaRef,
containerRef,
isPlaying,
}: UsePlayerControlsOptions): UsePlayerControlsReturn {
const [showControls, setShowControls] = useState(true);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const scheduleHide = useCallback(() => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
if (isPlaying) {
hideTimeoutRef.current = setTimeout(() => setShowControls(false), 2500);
}
}, [isPlaying]);
const revealControls = useCallback(() => {
setShowControls(true);
scheduleHide();
}, [scheduleHide]);
useEffect(() => {
if (isPlaying) {
scheduleHide();
} else {
setShowControls(true);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
}
return () => { if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); };
}, [isPlaying, scheduleHide]);
// Pause when scrolled out of view
useEffect(() => {
const media = mediaRef.current;
const container = containerRef.current;
if (!media || !container) return;
const observer = new IntersectionObserver(
([entry]) => { if (!entry.isIntersecting && !media.paused) media.pause(); },
{ threshold: 0.25 },
);
observer.observe(container);
return () => observer.disconnect();
}, [mediaRef, containerRef]);
// ── Volume ─────────────────────────────────────────────────────────────
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(1);
const prevVolumeRef = useRef(1);
// Sync initial muted/volume state from the media element (e.g. when the
// <video> has a `muted` attribute for autoplay).
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setIsMuted(media.muted);
setVolume(media.muted ? 0 : media.volume);
}, [mediaRef]);
const toggleMute = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const media = mediaRef.current;
if (!media) return;
if (media.muted || media.volume === 0) {
const restored = prevVolumeRef.current > 0 ? prevVolumeRef.current : 0.5;
media.muted = false;
media.volume = restored;
setIsMuted(false);
setVolume(restored);
} else {
prevVolumeRef.current = media.volume;
media.muted = true;
setIsMuted(true);
}
}, [mediaRef]);
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
const media = mediaRef.current;
if (!media) return;
const v = parseFloat(e.target.value);
media.volume = v;
media.muted = v === 0;
if (v > 0) prevVolumeRef.current = v;
setVolume(v);
setIsMuted(v === 0);
}, [mediaRef]);
return { showControls, revealControls, scheduleHide, isMuted, volume, toggleMute, handleVolumeChange };
}