aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
571 lines (568 loc) • 22.8 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useRef, useState, useEffect, useCallback } from 'react';
import { GlassCore } from '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useMedia } from './GlassMediaProvider.js';
const formatTime = seconds => {
if (isNaN(seconds)) return "0:00";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor(seconds % 3600 / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
} else {
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
};
const VideoControls = ({
isPlaying,
currentTime,
duration,
volume,
playbackRate,
isFullscreen,
isMuted,
quality,
buffered,
onPlayPause,
onSeek,
onVolumeChange,
onPlaybackRateChange,
onQualityChange,
onFullscreenToggle,
onMuteToggle
}) => {
const [showVolumeSlider, setShowVolumeSlider] = useState(false);
const [showSpeedMenu, setShowSpeedMenu] = useState(false);
const [showQualityMenu, setShowQualityMenu] = useState(false);
const [seeking, setSeeking] = useState(false);
const handleProgressClick = e => {
const rect = e.currentTarget.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
const time = percent * duration;
onSeek(time);
};
const handleProgressDrag = e => {
if (seeking) {
const rect = e.currentTarget.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
const time = Math.max(0, Math.min(duration, percent * duration));
onSeek(time);
}
};
const playbackSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
const qualities = ["auto", "144p", "240p", "360p", "480p", "720p", "1080p"];
return jsxs("div", {
"data-glass-component": true,
className: 'absolute bottom-0 left-0 right-0 glass-gradient-primary glass-gradient-primary via-black/40 glass-gradient-primary glass-p-4',
children: [jsxs("div", {
className: 'glass-w-full h-2 glass-surface-subtle/20 glass-radius-full cursor-pointer mb-4 relative',
onClick: handleProgressClick,
onMouseMove: handleProgressDrag,
onMouseDown: () => setSeeking(true),
onMouseUp: () => setSeeking(false),
onMouseLeave: () => setSeeking(false),
children: [jsx("div", {
className: 'absolute top-0 left-0 glass-h-full glass-surface-subtle/30 glass-radius-full',
style: {
width: `${buffered / duration * 100}%`
}
}), jsx("div", {
className: 'absolute top-0 left-0 glass-h-full glass-surface-blue glass-radius-full',
style: {
width: `${currentTime / duration * 100}%`
}
}), jsx("div", {
className: 'absolute glass-top-1/2 transform -translate-y-1/2 w-4 h-4 glass-surface-blue glass-radius-full glass-border-2 glass-border-white glass-shadow-md',
style: {
left: `calc(${currentTime / duration * 100}% - 8px)`
}
})]
}), jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between text-primary',
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsx("button", {
onClick: onPlayPause,
className: 'w-12 h-12 glass-flex glass-items-center glass-justify-center glass-surface-subtle/20 hover:glass-surface-subtle/30 glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: isPlaying ? jsxs("div", {
className: "glass-flex glass-gap-1",
children: [jsx("div", {
className: 'w-1 h-4 glass-surface-subtle glass-radius-full'
}), jsx("div", {
className: 'w-1 h-4 glass-surface-subtle glass-radius-full'
})]
}) : jsx("div", {
className: 'w-0 h-0 glass-border-l-4 glass-border-l-white glass-border-y-4 glass-border-y-transparent ml-1'
})
}), jsxs("div", {
className: 'relative',
onMouseEnter: () => setShowVolumeSlider(true),
onMouseLeave: () => setShowVolumeSlider(false),
children: [jsx("button", {
onClick: onMuteToggle,
className: 'w-10 h-10 glass-flex glass-items-center glass-justify-center hover:glass-surface-subtle/20 glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: isMuted || volume === 0 ? "🔇" : volume < 0.5 ? "🔉" : "🔊"
}), showVolumeSlider && jsx("div", {
className: 'absolute bottom-12 left-1/2 transform -translate-x-1/2 glass-surface-dark/80 glass-radius-lg glass-p-2',
children: jsx("input", {
type: "range",
min: "0",
max: "1",
step: "0.01",
value: isMuted ? 0 : volume,
onChange: e => onVolumeChange(Number(e.target.value)),
className: 'w-16 h-1 glass-surface-subtle/20 glass-radius-full appearance-none slider-thumb-white glass-focus glass-touch-target glass-contrast-guard'
})
})]
}), jsxs("div", {
className: 'glass-text-sm font-mono',
children: [formatTime(currentTime), " / ", formatTime(duration)]
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsxs("div", {
className: 'relative',
children: [jsxs("button", {
onClick: () => setShowSpeedMenu(!showSpeedMenu),
className: 'glass-px-3 glass-py-2 glass-text-sm hover:glass-surface-subtle/20 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: [playbackRate, "x"]
}), showSpeedMenu && jsx("div", {
className: 'absolute bottom-12 right-0 glass-surface-dark/90 glass-radius-lg glass-p-2 glass-min-w-20',
children: playbackSpeeds.map(speed => jsxs("button", {
onClick: () => {
onPlaybackRateChange(speed);
setShowSpeedMenu(false);
},
className: cn("block w-full text-left px-3 py-2 text-sm rounded hover:bg-white/20 transition-colors glass-focus glass-touch-target glass-contrast-guard", speed === playbackRate && "bg-blue-500"),
children: [speed, "x"]
}, speed))
})]
}), jsxs("div", {
className: 'relative',
children: [jsx("button", {
onClick: () => setShowQualityMenu(!showQualityMenu),
className: 'glass-px-3 glass-py-2 glass-text-sm hover:glass-surface-subtle/20 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: quality
}), showQualityMenu && jsx("div", {
className: 'absolute bottom-12 right-0 glass-surface-dark/90 glass-radius-lg glass-p-2 glass-min-w-20',
children: qualities.map(q => jsx("button", {
onClick: () => {
onQualityChange(q);
setShowQualityMenu(false);
},
className: cn("block w-full text-left px-3 py-2 text-sm rounded hover:bg-white/20 transition-colors glass-focus glass-touch-target glass-contrast-guard", q === quality && "bg-blue-500"),
children: q
}, q))
})]
}), jsx("button", {
onClick: onFullscreenToggle,
className: 'w-10 h-10 glass-flex glass-items-center glass-justify-center hover:glass-surface-subtle/20 glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: isFullscreen ? "⊟" : "⊞"
})]
})]
})]
});
};
const ChapterList = ({
chapters,
currentTime,
onChapterClick
}) => {
return jsxs("div", {
className: 'glass-surface-subtle text-primary glass-p-4 glass-radius-lg',
children: [jsx("h3", {
className: 'glass-text-lg font-semibold mb-4',
children: "Chapters"
}), jsx("div", {
className: 'space-y-2 max-h-96 overflow-y-auto',
children: chapters.map(chapter => {
const isActive = currentTime >= chapter.startTime && currentTime <= chapter.endTime;
return jsxs("button", {
onClick: () => onChapterClick(chapter),
className: cn("flex items-center gap-3 w-full p-3 rounded-lg text-left transition-colors glass-focus glass-touch-target glass-contrast-guard", isActive ? "bg-blue-600" : "bg-gray-800 hover:bg-gray-700"),
children: [chapter.thumbnail && jsx("img", {
src: chapter.thumbnail,
alt: chapter.title,
className: 'w-16 h-9 object-cover glass-radius'
}), jsxs("div", {
className: "glass-flex-1",
children: [jsx("div", {
className: 'font-medium',
children: chapter.title
}), jsxs("div", {
className: "glass-text-sm glass-text-secondary",
children: [formatTime(chapter.startTime), " -", " ", formatTime(chapter.endTime)]
}), chapter.description && jsx("div", {
className: 'glass-text-xs glass-text-secondary mt-1',
children: chapter.description
})]
})]
}, chapter.id);
})
})]
});
};
const TranscriptPanel = ({
transcript,
currentTime,
searchQuery,
onSearchChange,
onTranscriptClick
}) => {
const [highlightedResults, setHighlightedResults] = useState([]);
useEffect(() => {
if (searchQuery) {
const results = transcript.filter(entry => entry.text.toLowerCase().includes(searchQuery.toLowerCase()));
setHighlightedResults(results);
} else {
setHighlightedResults([]);
}
}, [searchQuery, transcript]);
const highlightText = (text, query) => {
if (!query) return text;
const regex = new RegExp(`(${query})`, "gi");
return text.split(regex).map((part, index) => part.toLowerCase() === query.toLowerCase() ? jsx("mark", {
className: "glass-surface-subtle glass-text-inverse glass-radius glass-px-1",
children: part
}, index) : part);
};
return jsxs("div", {
className: 'glass-surface-subtle text-primary glass-p-4 glass-radius-lg glass-h-full glass-flex glass-flex-col',
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-4',
children: [jsx("h3", {
className: 'glass-text-lg font-semibold',
children: "Transcript"
}), jsxs("div", {
className: "glass-text-sm glass-text-secondary",
children: [transcript.length, " entries"]
})]
}), jsxs("div", {
className: 'mb-4',
children: [jsx("input", {
type: "text",
placeholder: "Search transcript...",
value: searchQuery,
onChange: e => onSearchChange(e.target.value),
className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-primary glass-border glass-border-gray-700 glass-radius-lg text-primary placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 glass-focus glass-touch-target glass-contrast-guard'
}), searchQuery && jsxs("div", {
className: 'glass-text-sm glass-text-secondary mt-2',
children: [highlightedResults.length, " results found"]
})]
}), jsx("div", {
className: 'glass-flex-1 overflow-y-auto space-y-3',
children: (searchQuery ? highlightedResults : transcript).map(entry => {
const isActive = currentTime >= entry.startTime && currentTime <= entry.endTime;
const confidence = entry.confidence || 0;
return jsxs("button", {
onClick: () => onTranscriptClick(entry),
className: cn("flex flex-col items-start gap-2 w-full p-3 rounded-lg text-left transition-colors glass-focus glass-touch-target glass-contrast-guard", isActive ? "bg-blue-600" : "bg-gray-800 hover:bg-gray-700"),
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between glass-w-full",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: 'glass-text-xs glass-text-secondary font-mono',
children: formatTime(entry.startTime)
}), entry.speaker && jsx("span", {
className: 'glass-text-xs glass-surface-subtle text-gray-300 glass-px-2 glass-py-1 glass-radius',
children: entry.speaker
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [confidence > 0 && jsxs("div", {
className: cn("text-xs px-2 py-1 rounded", confidence > 0.9 ? "bg-green-600" : confidence > 0.7 ? "bg-yellow-600" : "bg-red-600"),
children: [Math.round(confidence * 100), "%"]
}), entry.sentiment && jsx("div", {
className: "glass-text-xs",
children: entry.sentiment === "positive" ? "😊" : entry.sentiment === "negative" ? "😔" : "😐"
})]
})]
}), jsx("div", {
className: 'glass-text-sm leading-relaxed',
children: searchQuery ? highlightText(entry.text, searchQuery) : entry.text
}), entry.keywords && entry.keywords.length > 0 && jsx("div", {
className: 'glass-flex glass-flex-wrap glass-gap-1 mt-2',
children: entry.keywords.map(keyword => jsx("span", {
className: 'glass-text-xs glass-surface-subtle text-gray-300 glass-px-2 glass-py-1 glass-radius',
children: keyword
}, keyword))
})]
}, entry.id);
})
})]
});
};
const GlassAdvancedVideoPlayer = ({
mediaFile,
className,
showControls = true,
showTranscript = false,
showChapters = false,
showAnalytics = false,
autoplay = false,
muted = false,
loop = false,
preload = "metadata",
poster,
onTimeUpdate,
onEnded,
onError
}) => {
const {
playbackState,
setPlaybackState,
play,
pause,
seekTo,
setVolume,
setPlaybackRate,
toggleMute,
toggleFullscreen,
setQuality,
transcripts,
generateTranscript,
searchTranscript,
getTranscriptAtTime,
setActiveChapter,
getChapterAtTime,
trackView,
trackEngagement
} = useMedia();
const videoRef = useRef(null);
const containerRef = useRef(null);
const [showControlsOverlay, setShowControlsOverlay] = useState(showControls);
const [controlsTimeout, setControlsTimeout] = useState(null);
const [transcriptSearch, setTranscriptSearch] = useState("");
const [hasTranscript, setHasTranscript] = useState(false);
const isPlaying = playbackState?.mediaId === mediaFile.id && playbackState.isPlaying;
const currentTime = playbackState?.mediaId === mediaFile.id ? playbackState.currentTime : 0;
const duration = playbackState?.mediaId === mediaFile.id ? playbackState.duration : mediaFile.duration || 0;
const volume = playbackState?.volume || 1;
const playbackRate = playbackState?.playbackRate || 1;
const isFullscreen = playbackState?.isFullscreen || false;
const isMuted = playbackState?.isMuted || false;
const quality = playbackState?.quality || "auto";
// Initialize transcript
useEffect(() => {
if (!transcripts[mediaFile.id] && showTranscript) {
generateTranscript(mediaFile.id).then(() => {
setHasTranscript(true);
});
} else if (transcripts[mediaFile.id]) {
setHasTranscript(true);
}
}, [mediaFile.id, showTranscript, generateTranscript, transcripts]);
// Mouse movement handling for controls
const handleMouseMove = useCallback(() => {
setShowControlsOverlay(true);
if (controlsTimeout) {
clearTimeout(controlsTimeout);
}
const timeout = setTimeout(() => {
if (isPlaying) {
setShowControlsOverlay(false);
}
}, 3000);
setControlsTimeout(timeout);
}, [isPlaying, controlsTimeout]);
// Video event handlers
const handleTimeUpdate = () => {
if (videoRef.current) {
const currentTime = videoRef.current.currentTime;
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
currentTime
});
}
onTimeUpdate?.(currentTime);
// Update active chapter
const activeChapter = getChapterAtTime(mediaFile.id, currentTime);
if (activeChapter) {
setActiveChapter(mediaFile.id, activeChapter.id);
}
}
};
const handleLoadedMetadata = () => {
if (videoRef.current) {
const duration = videoRef.current.duration;
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
duration,
isLoading: false
});
}
}
};
const handleEnded = () => {
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
isPlaying: false,
currentTime: 0
});
}
trackView(mediaFile.id, duration);
onEnded?.();
};
const handleError = () => {
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
isError: true,
isLoading: false
});
}
onError?.("Video playback error occurred");
};
const handlePlayPause = () => {
if (isPlaying) {
pause();
if (videoRef.current) {
videoRef.current.pause();
}
} else {
play(mediaFile.id);
if (videoRef.current) {
videoRef.current.play();
}
}
};
const handleSeek = time => {
seekTo(time);
if (videoRef.current) {
videoRef.current.currentTime = time;
}
};
const handleVolumeChange = newVolume => {
setVolume(newVolume);
if (videoRef.current) {
videoRef.current.volume = newVolume;
}
};
const handlePlaybackRateChange = rate => {
setPlaybackRate(rate);
if (videoRef.current) {
videoRef.current.playbackRate = rate;
}
};
const handleMuteToggle = () => {
toggleMute();
if (videoRef.current) {
videoRef.current.muted = !isMuted;
}
};
const handleFullscreenToggle = () => {
toggleFullscreen();
if (!isFullscreen) {
if (containerRef.current?.requestFullscreen) {
containerRef.current.requestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
};
const handleChapterClick = chapter => {
handleSeek(chapter.startTime);
setActiveChapter(mediaFile.id, chapter.id);
};
const handleTranscriptClick = entry => {
handleSeek(entry.startTime);
};
return jsx("div", {
className: cn("relative bg-black rounded-lg overflow-hidden", className),
children: jsxs("div", {
ref: containerRef,
className: 'relative glass-w-full glass-h-full glass-flex',
onMouseMove: handleMouseMove,
onMouseLeave: () => setShowControlsOverlay(false),
children: [jsxs("div", {
className: 'glass-flex-1 relative',
children: [jsx("video", {
ref: videoRef,
src: mediaFile.src,
poster: poster || mediaFile.poster,
autoPlay: autoplay,
muted: muted,
loop: loop,
preload: preload,
className: 'glass-w-full glass-h-full object-contain',
onTimeUpdate: handleTimeUpdate,
onLoadedMetadata: handleLoadedMetadata,
onEnded: handleEnded,
onError: handleError,
onClick: handlePlayPause
}), playbackState?.isLoading && jsx("div", {
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center glass-surface-dark/50',
children: jsx("div", {
className: 'animate-spin glass-radius-full h-16 w-16 glass-border-4 glass-border-white glass-border-t-transparent'
})
}), playbackState?.isError && jsx("div", {
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center glass-surface-dark/80 text-primary',
children: jsxs("div", {
className: 'text-center',
children: [jsx("div", {
className: 'glass-text-4xl mb-4',
children: "\u26A0\uFE0F"
}), jsx("h3", {
className: 'glass-text-xl font-semibold mb-2',
children: "Playback Error"
}), jsx("p", {
className: 'text-gray-300',
children: "Unable to load video content"
})]
})
}), showControlsOverlay && jsx(VideoControls, {
isPlaying: isPlaying,
currentTime: currentTime,
duration: duration,
volume: volume,
playbackRate: playbackRate,
isFullscreen: isFullscreen,
isMuted: isMuted,
quality: quality,
buffered: videoRef.current?.buffered.length ? videoRef.current.buffered.end(videoRef.current.buffered.length - 1) : 0,
onPlayPause: handlePlayPause,
onSeek: handleSeek,
onVolumeChange: handleVolumeChange,
onPlaybackRateChange: handlePlaybackRateChange,
onQualityChange: setQuality,
onFullscreenToggle: handleFullscreenToggle,
onMuteToggle: handleMuteToggle
})]
}), jsxs("div", {
className: "glass-flex",
children: [showChapters && mediaFile.chapters && mediaFile.chapters.length > 0 && jsx(GlassCore, {
className: 'w-80 glass-h-full overflow-hidden',
children: jsx(ChapterList, {
chapters: mediaFile.chapters,
currentTime: currentTime,
onChapterClick: handleChapterClick
})
}), showTranscript && hasTranscript && transcripts[mediaFile.id] && jsx(GlassCore, {
className: 'w-96 glass-h-full overflow-hidden',
children: jsx(TranscriptPanel, {
transcript: transcripts[mediaFile.id],
currentTime: currentTime,
searchQuery: transcriptSearch,
onSearchChange: setTranscriptSearch,
onTranscriptClick: handleTranscriptClick
})
})]
})]
})
});
};
export { GlassAdvancedVideoPlayer };
//# sourceMappingURL=GlassAdvancedVideoPlayer.js.map