@react-three/uikit-default
Version:
Default (shadcn) kit for @react-three/uikit
83 lines (82 loc) • 5.13 kB
JavaScript
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')}`;
}