UNPKG

@react-three/uikit-default

Version:

Default (shadcn) kit for @react-three/uikit

83 lines (82 loc) 5.13 kB
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { computed, signal } from '@preact/signals-core'; import { Container, Video as VideoImpl, Text, useVideoElement, } from '@react-three/uikit'; import { Play, Pause, VolumeX, Volume2 } from '@react-three/uikit-lucide'; import { Slider } from './slider.js'; import { Button } from './button.js'; import { colors } from './theme.js'; const movingContext = createContext(undefined); export const Video = forwardRef(({ controls, children, ...rest }, ref) => { const moving = useMemo(() => signal(false), []); const handlers = useMemo(() => { let timeoutRef; const onInteract = () => { moving.value = true; if (timeoutRef != null) { clearTimeout(timeoutRef); } timeoutRef = setTimeout(() => (moving.value = false), 2000); }; return { onPointerMove: onInteract, onPointerDown: onInteract, }; }, [moving]); return (React.createElement(VideoImpl, { ...rest, ...handlers, positionType: "relative", ref: ref }, React.createElement(movingContext.Provider, { value: moving }, controls && React.createElement(VideoControls, null)), children)); }); export const VideoControls = forwardRef((props, ref) => { const videoElement = useVideoElement(); const [paused, setPaused] = useState(videoElement.paused); useEffect(() => { const listener = () => setPaused(videoElement.paused); videoElement.addEventListener('pause', listener); videoElement.addEventListener('play', listener); return () => { videoElement.removeEventListener('pause', listener); videoElement.removeEventListener('play', listener); }; }, [videoElement]); const [muted, setMuted] = useState(videoElement.muted); useEffect(() => { const listener = () => setMuted(videoElement.muted); videoElement.addEventListener('volumechange', listener); return () => videoElement.removeEventListener('volumechange', listener); }, [videoElement]); const durationSignal = useMemo(() => signal(1), []); const timeSignal = useMemo(() => signal(0), []); const moving = useContext(movingContext); if (moving == null) { throw new Error(`VideoControls form the default kit can only be used inside a Video from the default kit`); } const displaySignal = useMemo(() => computed(() => (moving.value ? 'flex' : 'none')), [moving]); useEffect(() => { const metadataListener = () => (durationSignal.value = videoElement.duration); const timeUpdateListener = () => (timeSignal.value = videoElement.currentTime); if (!isNaN(videoElement.duration)) { metadataListener(); } videoElement.addEventListener('loadedmetadata', metadataListener); videoElement.addEventListener('timeupdate', timeUpdateListener); return () => { videoElement.removeEventListener('loadedmetadata', metadataListener); videoElement.removeEventListener('timeupdate', timeUpdateListener); }; }, [durationSignal, timeSignal, videoElement]); const timeTextSignal = useMemo(() => computed(() => `${formatDuration(timeSignal.value)} / ${formatDuration(durationSignal.value)}`), [durationSignal, timeSignal]); const setTime = useCallback((t) => (videoElement.currentTime = t), [videoElement]); return (React.createElement(Container, { display: displaySignal, positionType: "absolute", padding: 8, positionBottom: 0, positionLeft: 0, positionRight: 0, flexDirection: "column", backgroundOpacity: 0.5, backgroundColor: colors.background, gap: 8, ...props, ref: ref }, React.createElement(Container, { flexDirection: "row", alignItems: "center" }, React.createElement(Button, { size: "icon", variant: "ghost", marginRight: 8, onClick: () => (paused ? videoElement.play() : videoElement.pause()) }, paused ? (React.createElement(Play, { cursor: "pointer", width: 16, height: 16 })) : (React.createElement(Pause, { cursor: "pointer", width: 16, height: 16 }))), React.createElement(Button, { size: "icon", variant: "ghost", marginRight: 8, onClick: () => (videoElement.muted = !muted) }, muted ? (React.createElement(VolumeX, { cursor: "pointer", width: 16, height: 16 })) : (React.createElement(Volume2, { cursor: "pointer", width: 16, height: 16 }))), React.createElement(Container, { flexGrow: 1 }), React.createElement(Text, { marginRight: 16, fontSize: 12 }, timeTextSignal)), React.createElement(Slider, { min: 0, margin: 16, marginTop: 8, width: undefined, max: durationSignal, value: timeSignal, onValueChange: setTime }))); }); function formatDuration(seconds) { const hour = Math.floor(seconds / 3600); const min = Math.floor((seconds / 60) % 60); const sec = Math.floor(seconds % 60); return `${hour > 0 ? `${hour}:` : ''}${hour > 0 ? min.toString().padStart(2, '0') : min}:${sec.toString().padStart(2, '0')}`; }