aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
430 lines (427 loc) • 16.8 kB
JavaScript
'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