UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

430 lines (427 loc) 16.8 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { Loader2, AlertCircle, Play, Minimize, Maximize, Settings, SkipBack, Pause, SkipForward, VolumeX, Volume2 } from 'lucide-react'; import { useRef, useState, useCallback, useEffect } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardContent } from '../card/index.js'; import '../data-display/GlassAccordion.js'; import '../data-display/GlassAlert.js'; import '../data-display/GlassAvatar.js'; import { GlassBadge } from '../data-display/GlassBadge.js'; import '../data-display/GlassBadgeLine.js'; import '../data-display/GlassDataGrid.js'; import '../data-display/GlassDataTable.js'; import '../data-display/GlassHeatmap.js'; import '../data-display/GlassLoadingSkeleton.js'; import '../data-display/GlassProgress.js'; import '../data-display/GlassTimeline.js'; import '../data-display/GlassSkeleton.js'; import '../data-display/GlassNotificationCenter.js'; import '../data-display/GlassAnimatedNumber.js'; import { GlassCard } from '../card/GlassCard.js'; /** * GlassVideoPlayer component * A comprehensive video player with glassmorphism design and advanced controls */ const GlassVideoPlayer = ({ sources, poster, title, autoPlay = false, controls = true, enableFullscreen = true, enablePiP = true, enableTheaterMode = false, subtitles = [], playbackSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], className, onPlay, onPause, onTimeUpdate, onVolumeChange, onFullscreenChange, ...props }) => { const videoRef = useRef(null); const containerRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [isTheaterMode, setIsTheaterMode] = useState(false); const [showControls, setShowControls] = useState(true); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useState("auto"); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [showSettings, setShowSettings] = useState(false); // Hide controls after inactivity const controlsTimeoutRef = useRef(); // Handle play/pause const handlePlayPause = useCallback(async () => { if (!videoRef.current) return; try { if (isPlaying) { await videoRef.current.pause(); setIsPlaying(false); onPause?.(); } else { await videoRef.current.play(); setIsPlaying(true); onPlay?.(); } } catch (err) { console.error("Playback error:", err); } }, [isPlaying, onPlay, onPause]); // Handle time update const handleTimeUpdate = useCallback(() => { if (!videoRef.current) return; const current = videoRef.current.currentTime; const total = videoRef.current.duration || 0; setCurrentTime(current); setDuration(total); onTimeUpdate?.(current, total); }, [onTimeUpdate]); // Handle seek const handleSeek = useCallback(e => { if (!videoRef.current || !containerRef.current) return; const rect = e.currentTarget.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; const newTime = percent * duration; videoRef.current.currentTime = newTime; setCurrentTime(newTime); }, [duration]); // Handle volume change const handleVolumeChange = useCallback(newVolume => { if (!videoRef.current) return; const clampedVolume = Math.max(0, Math.min(1, newVolume)); videoRef.current.volume = clampedVolume; setVolume(clampedVolume); setIsMuted(clampedVolume === 0); onVolumeChange?.(clampedVolume, clampedVolume === 0); }, [onVolumeChange]); // Handle mute toggle const handleMuteToggle = useCallback(() => { if (!videoRef.current) return; const newMuted = !isMuted; videoRef.current.muted = newMuted; setIsMuted(newMuted); onVolumeChange?.(newMuted ? 0 : volume, newMuted); }, [isMuted, volume, onVolumeChange]); // Handle fullscreen toggle const handleFullscreenToggle = useCallback(async () => { if (!containerRef.current) return; try { if (!isFullscreen) { await containerRef.current.requestFullscreen(); setIsFullscreen(true); } else { await document.exitFullscreen(); setIsFullscreen(false); } onFullscreenChange?.(!isFullscreen); } catch (err) { console.error("Fullscreen error:", err); } }, [isFullscreen, onFullscreenChange]); // Handle keyboard shortcuts useEffect(() => { const handleKeyPress = e => { if (!videoRef.current) return; switch (e.key.toLowerCase()) { case " ": e.preventDefault(); handlePlayPause(); break; case "arrowleft": e.preventDefault(); videoRef.current.currentTime = Math.max(0, currentTime - 10); break; case "arrowright": e.preventDefault(); videoRef.current.currentTime = Math.min(duration, currentTime + 10); break; case "arrowup": e.preventDefault(); handleVolumeChange(volume + 0.1); break; case "arrowdown": e.preventDefault(); handleVolumeChange(volume - 0.1); break; case "m": e.preventDefault(); handleMuteToggle(); break; case "f": e.preventDefault(); if (enableFullscreen) handleFullscreenToggle(); break; } }; document.addEventListener("keydown", handleKeyPress); return () => document.removeEventListener("keydown", handleKeyPress); }, [handlePlayPause, handleVolumeChange, handleMuteToggle, handleFullscreenToggle, currentTime, duration, volume, enableFullscreen]); // Auto-hide controls const resetControlsTimeout = useCallback(() => { if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); } setShowControls(true); if (isPlaying) { controlsTimeoutRef.current = setTimeout(() => { setShowControls(false); }, 3000); } }, [isPlaying]); useEffect(() => { resetControlsTimeout(); return () => { if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); } }; }, [resetControlsTimeout]); // Handle mouse movement to show controls const handleMouseMove = useCallback(() => { resetControlsTimeout(); }, [resetControlsTimeout]); // Format time const formatTime = useCallback(time => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes}:${seconds.toString().padStart(2, "0")}`; }, []); // Handle video load const handleLoadedData = useCallback(() => { setIsLoading(false); setError(null); }, []); // Handle video error const handleError = useCallback(() => { setIsLoading(false); setError("Failed to load video"); }, []); // Skip forward/backward const handleSkip = useCallback(seconds => { if (!videoRef.current) return; videoRef.current.currentTime = Math.max(0, Math.min(duration, currentTime + seconds)); }, [currentTime, duration]); if (sources.length === 0) { return jsx(GlassCard, { "data-glass-component": true, className: cn("p-8", className), children: jsx("div", { className: 'text-center text-primary/60', children: "No video sources provided" }) }); } return jsx(MotionFramer, { preset: "fadeIn", className: "glass-w-full", children: jsx(GlassCard, { className: cn("overflow-hidden relative group", isFullscreen && "fixed inset-0 z-50 rounded-none", isTheaterMode && "aspect-video max-w-6xl mx-auto", className), ref: containerRef, onMouseMove: handleMouseMove, ...props, children: jsx(CardContent, { className: "glass-p-0", children: jsxs("div", { className: 'relative glass-surface-dark aspect-video overflow-hidden', children: [jsxs("video", { ref: videoRef, className: 'glass-w-full glass-h-full object-contain', poster: poster, autoPlay: autoPlay, onLoadedData: handleLoadedData, onTimeUpdate: handleTimeUpdate, onPlay: () => setIsPlaying(true), onPause: () => setIsPlaying(false), onError: handleError, onWaiting: () => setIsLoading(true), onCanPlay: () => setIsLoading(false), children: [sources.map((source, index) => jsx("source", { src: source.src, type: source.type }, index)), subtitles.map((subtitle, index) => jsx("track", { src: subtitle.src, label: subtitle.label, kind: "subtitles", srcLang: subtitle.language, default: subtitle.default }, index)), "Your browser does not support the video tag."] }), isLoading && jsx("div", { className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center glass-surface-dark/50', children: jsx(Loader2, { className: 'w-8 h-8 text-primary animate-spin' }) }), error && jsxs("div", { className: 'absolute inset-0 glass-flex glass-flex-col glass-items-center glass-justify-center glass-surface-dark/50 text-primary', children: [jsx(AlertCircle, { className: 'w-12 h-12 mb-4' }), jsx("p", { children: error })] }), !isPlaying && !isLoading && !error && jsx("div", { className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center', children: jsx(GlassButton, { variant: "secondary", size: "lg", onClick: handlePlayPause, className: "glass-p-6 glass-radius-full", children: jsx(Play, { className: 'w-8 h-8' }) }) }), controls && showControls && jsxs("div", { className: 'absolute inset-0 glass-gradient-primary glass-gradient-primary via-transparent glass-gradient-primary', children: [jsxs("div", { className: 'absolute top-0 left-0 right-0 glass-p-4 glass-flex glass-justify-between glass-items-center', children: [title && jsx(GlassBadge, { variant: "secondary", className: "glass-surface-dark/50", children: title }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [enableTheaterMode && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setIsTheaterMode(!isTheaterMode), className: 'text-primary', children: isTheaterMode ? jsx(Minimize, { className: 'w-4 h-4' }) : jsx(Maximize, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setShowSettings(!showSettings), className: 'text-primary', children: jsx(Settings, { className: 'w-4 h-4' }) })] })] }), jsx("div", { className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center', children: jsxs("div", { className: 'glass-flex glass-items-center glass-gap-4 opacity-0 group-hover:opacity-100 transition-opacity', children: [jsx(GlassButton, { variant: "secondary", size: "lg", onClick: e => handleSkip(-10), className: "glass-p-3", children: jsx(SkipBack, { className: 'w-6 h-6' }) }), jsx(GlassButton, { variant: "secondary", size: "lg", onClick: handlePlayPause, className: "glass-p-4", children: isPlaying ? jsx(Pause, { className: 'w-8 h-8' }) : jsx(Play, { className: 'w-8 h-8' }) }), jsx(GlassButton, { variant: "secondary", size: "lg", onClick: e => handleSkip(10), className: "glass-p-3", children: jsx(SkipForward, { className: 'w-6 h-6' }) })] }) }), jsxs("div", { className: 'absolute bottom-0 left-0 right-0 glass-p-4', children: [jsx("div", { className: 'glass-w-full h-1 glass-surface-subtle/20 glass-radius-full mb-4 cursor-pointer', onClick: handleSeek, children: jsx("div", { className: 'glass-h-full glass-surface-primary glass-radius-full transition-all duration-100', style: { width: `${duration > 0 ? currentTime / duration * 100 : 0}%` } }) }), jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-4", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handlePlayPause, className: 'text-primary glass-p-2', children: isPlaying ? jsx(Pause, { className: 'w-5 h-5' }) : jsx(Play, { className: 'w-5 h-5' }) }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleMuteToggle, className: 'text-primary glass-p-2', children: isMuted || volume === 0 ? jsx(VolumeX, { className: 'w-5 h-5' }) : jsx(Volume2, { className: 'w-5 h-5' }) }), jsx("div", { className: 'w-20 h-1 glass-surface-subtle/20 glass-radius-full cursor-pointer', children: jsx("div", { className: "glass-h-full glass-surface-subtle glass-radius-full", style: { width: `${isMuted ? 0 : volume * 100}%` } }) })] }), jsxs("span", { className: 'text-primary glass-text-sm', children: [formatTime(currentTime), " / ", formatTime(duration)] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsxs("span", { className: 'text-primary glass-text-sm', children: [playbackSpeed, "x"] }), enableFullscreen && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleFullscreenToggle, className: 'text-primary glass-p-2', children: isFullscreen ? jsx(Minimize, { className: 'w-5 h-5' }) : jsx(Maximize, { className: 'w-5 h-5' }) })] })] })] })] })] }) }) }) }); }; export { GlassVideoPlayer, GlassVideoPlayer as default }; //# sourceMappingURL=GlassVideoPlayer.js.map