feat: add VideoViewer component for enhanced video playback experience; implement download functionality and viewer modal

This commit is contained in:
2026-04-24 08:38:22 +10:00
parent 35ccdc0a22
commit 7b69ed6df8
7 changed files with 192 additions and 128 deletions

View File

@@ -1,43 +1,20 @@
import React, { VideoHTMLAttributes, forwardRef, useRef, useState, useEffect } from 'react';
import classNames from 'classnames';
import { Box, IconButton, Icon, Icons, Tooltip, TooltipProvider, Text } from 'folds';
import * as css from './media.css';
import * as videoCss from './videoControls.css';
/**
* Fullscreen icon SVG
*/
function FullscreenIcon() {
return (
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
</svg>
);
}
/**
* Exit fullscreen icon SVG
*/
function ExitFullscreenIcon() {
return (
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
</svg>
);
}
type VideoProps = VideoHTMLAttributes<HTMLVideoElement> & {
disableControls?: boolean;
onExpand?: () => void;
};
export const Video = forwardRef<HTMLVideoElement, VideoProps>(
({ className, disableControls, ...props }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
({ className, disableControls, onExpand, ...props }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [showControls, setShowControls] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
// Combine refs
useEffect(() => {
if (typeof ref === 'function') {
ref(videoRef.current);
@@ -46,74 +23,22 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
}
}, [ref]);
// Track fullscreen state
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
if (!onExpand) return;
const video = videoRef.current;
if (!video) return;
const handleFullscreenRequest = (evt: Event) => {
evt.preventDefault();
evt.stopImmediatePropagation();
onExpand();
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const handleFullscreen = async () => {
try {
if (!document.fullscreenElement && containerRef.current) {
await containerRef.current.requestFullscreen();
} else if (document.fullscreenElement) {
await document.exitFullscreen();
}
} catch (error) {
console.error('Error toggling fullscreen:', error);
}
};
const handlePopOut = () => {
const videoSrc = videoRef.current?.src;
const videoTitle = props.title || 'Video';
if (videoSrc) {
const popoutWindow = window.open(
'',
'_blank',
'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'
);
if (popoutWindow) {
popoutWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${videoTitle}</title>
<style>
body {
margin: 0;
padding: 0;
background: #000;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
video {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<video controls autoplay src="${videoSrc}">
Your browser does not support the video tag.
</video>
</body>
</html>
`);
popoutWindow.document.close();
}
}
};
video.addEventListener('webkitbeginfullscreen', handleFullscreenRequest);
return () => {
video.removeEventListener('webkitbeginfullscreen', handleFullscreenRequest);
};
}, [onExpand]);
if (disableControls) {
return (
@@ -131,7 +56,6 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
return (
<Box
ref={containerRef}
className={videoCss.VideoContainer}
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
@@ -143,15 +67,17 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
ref={videoRef}
loop={props.autoPlay}
muted={props.autoPlay}
playsInline
playsInline
controlsList={onExpand ? 'nofullscreen' : undefined}
onDoubleClick={onExpand ? (e) => { e.stopPropagation(); onExpand(); } : undefined}
/>
{showControls && (
<Box className={videoCss.VideoControlsOverlay} gap="200">
{onExpand && (
<Box className={classNames(videoCss.VideoControlsOverlay, !showControls && videoCss.VideoControlsOverlayHidden)} gap="200">
<TooltipProvider
tooltip={
<Tooltip variant="Secondary">
<Text size="T200">{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}</Text>
<Text size="T200">Expand</Text>
</Tooltip>
}
position="Bottom"
@@ -163,36 +89,10 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
variant="Secondary"
size="300"
radii="300"
onClick={handleFullscreen}
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
onClick={onExpand}
aria-label="Expand video"
>
<Icon
size="200"
src={isFullscreen ? ExitFullscreenIcon : FullscreenIcon}
/>
</IconButton>
)}
</TooltipProvider>
<TooltipProvider
tooltip={
<Tooltip variant="Secondary">
<Text size="T200">Pop Out</Text>
</Tooltip>
}
position="Bottom"
align="Center"
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
variant="Secondary"
size="300"
radii="300"
onClick={handlePopOut}
aria-label="Open in new window"
>
<Icon size="200" src={Icons.External} />
<Icon size="50" src={Icons.Plus} />
</IconButton>
)}
</TooltipProvider>