import { Button } from '@/components/ui/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import useAdminVideoTimelineStore from '@/stores/AdminVideoTimelineStore'; import { ZoomInIcon, ZoomOutIcon } from 'lucide-react'; import React, { useEffect, useRef, useState } from 'react'; const VideoTimelineViewer = () => { const [videoUuid, setVideoUuid] = useState('c4fd0601-cac7-483b-907e-0ae4c108d3b1'); const [zoomLevel, setZoomLevel] = useState(100); // Default zoom level (100%) const { getVideoTimelines, videoTimelines, isLoadingVideoTimelines } = useAdminVideoTimelineStore(); const timelineContainerRef = useRef(null); // Define a dark theme for tooltips const tooltipStyle = { content: 'p-2 bg-gray-800 border border-gray-700 rounded shadow-md z-50 text-white', text: 'text-white', highlight: 'text-white font-semibold', }; // Define an array of 20 distinct Tailwind background colors const colorPresets = [ 'bg-blue-500', // Blue 'bg-green-500', // Green 'bg-purple-500', // Purple 'bg-red-500', // Red 'bg-yellow-500', // Yellow 'bg-pink-500', // Pink 'bg-indigo-500', // Indigo 'bg-teal-500', // Teal 'bg-orange-500', // Orange 'bg-cyan-500', // Cyan 'bg-lime-500', // Lime 'bg-emerald-500', // Emerald 'bg-sky-500', // Sky blue 'bg-violet-500', // Violet 'bg-fuchsia-500', // Fuchsia 'bg-rose-500', // Rose 'bg-amber-500', // Amber 'bg-blue-600', // Darker blue 'bg-green-600', // Darker green 'bg-purple-600', // Darker purple ]; // Use a ref to persist the color map between renders const colorMapRef = useRef(new Map()); // Get color based on element type const getElementColor = (type) => { // If we've already assigned a color to this type, use it if (colorMapRef.current.has(type)) { return colorMapRef.current.get(type); } // Otherwise, assign the next color in the sequence const currentCount = colorMapRef.current.size; const colorIndex = currentCount % colorPresets.length; const newColor = colorPresets[colorIndex]; // Store the mapping for future use colorMapRef.current.set(type, newColor); return newColor; }; const handleSubmit = (e) => { e.preventDefault(); if (videoUuid.trim()) { getVideoTimelines(videoUuid); } }; const handleZoomIn = () => { // Use different increments based on current zoom level if (zoomLevel < 50) { setZoomLevel((prev) => Math.min(prev + 5, 300)); // Smaller steps at low zoom } else { setZoomLevel((prev) => Math.min(prev + 25, 300)); // Larger steps at normal zoom } }; const handleZoomOut = () => { // Use different decrements based on current zoom level if (zoomLevel <= 50) { setZoomLevel((prev) => Math.max(prev - 5, 5)); // Smaller steps at low zoom, min 5% } else { setZoomLevel((prev) => Math.max(prev - 25, 5)); // Larger steps at normal zoom, min 5% } }; // Calculate the maximum duration for scaling const getMaxDuration = () => { if (!videoTimelines.length) return 7; // Default value let maxEnd = 0; videoTimelines.forEach((element) => { const elementEnd = element.time + element.duration; if (elementEnd > maxEnd) { maxEnd = elementEnd; } }); return maxEnd; }; // Get unique tracks for vertical positioning const getTracks = () => { if (!videoTimelines.length) return []; const tracks = [...new Set(videoTimelines.map((element) => element.track))]; return tracks.sort((a, b) => b - a); // Sort in descending order }; // Get track information for tooltips const getTrackInfo = (trackNum) => { const trackElements = videoTimelines.filter((element) => element.track === trackNum); if (trackElements.length === 0) { return { count: 0, duration: 0, startTime: 0, endTime: 0 }; } let totalDuration = 0; let minStartTime = Infinity; let maxEndTime = 0; trackElements.forEach((element) => { totalDuration += element.duration; if (element.time < minStartTime) minStartTime = element.time; const endTime = element.time + element.duration; if (endTime > maxEndTime) maxEndTime = endTime; }); return { count: trackElements.length, duration: totalDuration, startTime: minStartTime, endTime: maxEndTime, elements: trackElements, }; }; // Keep track of previous zoom level to calculate proper scrolling const prevZoomRef = useRef(zoomLevel); // Scroll timeline to position when zoom changes useEffect(() => { if (timelineContainerRef.current && videoTimelines.length > 0) { const container = timelineContainerRef.current; const prevZoom = prevZoomRef.current; // Calculate the center point of the current view const viewportWidth = container.clientWidth; const scrollLeft = container.scrollLeft; const centerPoint = scrollLeft + viewportWidth / 2; // Calculate what percentage of the timeline this center point represents const timelineWidth = container.firstChild.offsetWidth; const centerPercentage = centerPoint / ((timelineWidth * prevZoom) / zoomLevel); // Calculate the new scroll position to keep that same percentage in the center const newTimelineWidth = timelineWidth; const newCenterPoint = centerPercentage * newTimelineWidth; const newScrollPosition = newCenterPoint - viewportWidth / 2; // Update the scroll position requestAnimationFrame(() => { container.scrollLeft = Math.max(0, newScrollPosition); }); // Save current zoom for next comparison prevZoomRef.current = zoomLevel; } }, [zoomLevel, videoTimelines.length]); // Get label based on external reference const getElementLabel = (element) => { if (element.external_reference) { return element.external_reference.replace(/_/g, ' '); } return element.type; }; // Calculate time scale factor based on zoom level const getTimeScaleFactor = () => { // At 100%, scale factor is 1 // Below 100%, scale shrinks proportionally // Above 100%, scale grows proportionally return zoomLevel / 100; }; const maxDuration = getMaxDuration(); const tracks = getTracks(); const trackHeight = 40; // Height of each track row const timelineHeight = tracks.length * trackHeight; return ( Video UUID
setVideoUuid(e.target.value)} className="flex-1" />

Timeline Preview

{/* Zoom controls */}
{zoomLevel}%
Adjust zoom to change timeline scale
{/* Timeline container with horizontal scroll for small screens */}
{/* Timeline header - time markers with tick marks */}
Track
{/* Vertical guideline marks at each second */} {Array.from({ length: Math.ceil(maxDuration) + 1 }).map((_, i) => { const scaleFactor = zoomLevel / 100; const baseWidth = 100; // Base width in pixels at 100% zoom const position = i * baseWidth * scaleFactor; return (
{i}s
); })}
{/* Track rows */} {tracks.map((trackNum, index) => { const trackInfo = getTrackInfo(trackNum); return (

Track {trackNum}

Elements: {trackInfo.count}

Total duration: {trackInfo.duration.toFixed(2)}s

{trackInfo.count > 0 && ( <>

Start time: {trackInfo.startTime.toFixed(2)}s

End time: {trackInfo.endTime.toFixed(2)}s

)}
); })} {/* Timeline elements */} {videoTimelines.map((element) => { const trackIndex = tracks.indexOf(element.track); const scaleFactor = zoomLevel / 100; const baseWidth = 100; // Base width of 1 second at 100% zoom // Position from the exact timeline start (after the track label) const trackLabelWidth = 64; // 16rem = 64px on small screens, 80px on larger const exactStartPosition = element.time * baseWidth * scaleFactor; const startPosition = trackLabelWidth + exactStartPosition; const elementWidth = element.duration * baseWidth * scaleFactor; return (
{getElementLabel(element)}

{getElementLabel(element)}

Type: {element.type}

Track: {element.track}

Start: {element.time.toFixed(2)}s

Duration: {element.duration.toFixed(2)}s

End: {(element.time + element.duration).toFixed(2)}s

{element.id &&

ID: {element.id}

}
); })}
{/* Element details section */}

Element Details

{videoTimelines.map((element) => ( ))} {videoTimelines.length === 0 && ( )}
ID Type Reference Time Duration Track
{element.id} {element.type} {' '} {element.external_reference} {element.time}s {element.duration}s {element.track}
No timeline data available
{/* Dynamically generate color legend based on types that appear in the data */} {Array.from(colorMapRef.current.entries()).map(([type, color]) => (
{type}
))} {colorMapRef.current.size === 0 && ( <>
No elements
)}

Total elements: {videoTimelines.length}

|

Zoom: {zoomLevel}%

); }; export default VideoTimelineViewer;