import React, { useState, useEffect, useRef } from 'react'; import { Camera, Plus, Trash2, Edit3, Download, Play, Pause, ChevronDown, Settings, Save, X, FileText, Upload, RefreshCw, Globe, Volume2, Image, Clock, Copy, FileVideo, Check, AlertTriangle, Zap, Layers, UploadCloud, Grid, CheckCircle, Move, ArrowLeft, ArrowRight, SkipBack, SkipForward, Music, Mic, Eye, EyeOff, Maximize, Minimize, Monitor, Wind } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import * as d3 from 'd3'; // Mock imports for shadcn UI components import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Progress } from '@/components/ui/progress'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; // Animation presets for elements const animationPresets = { fadeIn: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.5 } }, slideIn: { initial: { x: -100, opacity: 0 }, animate: { x: 0, opacity: 1 }, exit: { x: 100, opacity: 0 }, transition: { duration: 0.5 } }, zoomIn: { initial: { scale: 0, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0, opacity: 0 }, transition: { duration: 0.5 } }, bounceIn: { initial: { scale: 0, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0, opacity: 0 }, transition: { type: "spring", stiffness: 200, damping: 10 } }, flipIn: { initial: { rotateY: 90, opacity: 0 }, animate: { rotateY: 0, opacity: 1 }, exit: { rotateY: -90, opacity: 0 }, transition: { duration: 0.5 } } }; // Helper for getting element animation based on time const getElementAnimationState = (element, currentTime) => { if (currentTime < element.start) { return "before"; } else if (currentTime >= element.start && currentTime < element.start + element.duration) { return "active"; } else { return "after"; } }; const VideoGeneratorApp = () => { // References const canvasRef = useRef(null); const videoRef = useRef(null); const timelineRef = useRef(null); const previewContainerRef = useRef(null); // State management const [activeTab, setActiveTab] = useState("templates"); const [currentTemplate, setCurrentTemplate] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [showEditor, setShowEditor] = useState(false); const [targetLanguage, setTargetLanguage] = useState("es-ES"); const [nativeLanguage, setNativeLanguage] = useState("en-US"); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(15); const [isDraggingTimeline, setIsDraggingTimeline] = useState(false); const [showWaveform, setShowWaveform] = useState(true); const [fullscreenPreview, setFullscreenPreview] = useState(false); const [exportProgress, setExportProgress] = useState(0); const [isExporting, setIsExporting] = useState(false); const [exportFormat, setExportFormat] = useState("mp4"); const [exportResolution, setExportResolution] = useState("1080p"); const [savedProjects, setSavedProjects] = useState([]); const [showLoadProjectDialog, setShowLoadProjectDialog] = useState(false); const [previewQuality, setPreviewQuality] = useState("medium"); const [draggedElement, setDraggedElement] = useState(null); const [content, setContent] = useState([ { id: 1, image: "medico.jpg", targetWord: "médico", nativeWord: "doctor", targetPhrase1: "quiere ser médico", nativePhrase1: "he wants to be a doctor", targetPhrase2: "su médico es bueno", nativePhrase2: "his doctor is good", audio: "medico-audio.mp3" }, { id: 2, image: "abogado.jpg", targetWord: "abogado", nativeWord: "lawyer", targetPhrase1: "estudia para abogado", nativePhrase1: "he studies to be a lawyer", targetPhrase2: "necesita un abogado", nativePhrase2: "he needs a lawyer", audio: "abogado-audio.mp3" } ]); const [currentContentIndex, setCurrentContentIndex] = useState(0); const [newRow, setNewRow] = useState({ image: "", targetWord: "", nativeWord: "", targetPhrase1: "", nativePhrase1: "", targetPhrase2: "", nativePhrase2: "" }); const [selectedAnimationStyle, setSelectedAnimationStyle] = useState("fadeIn"); const [audioSyncEnabled, setAudioSyncEnabled] = useState(true); const [generatingStatus, setGeneratingStatus] = useState(null); const [renderQueue, setRenderQueue] = useState([]); const [projectName, setProjectName] = useState("Vocabulary Project"); const [showNotification, setShowNotification] = useState(false); const [notificationMessage, setNotificationMessage] = useState(""); const [notificationType, setNotificationType] = useState("success"); const [editingElement, setEditingElement] = useState(null); const [audioData, setAudioData] = useState(null); const [audioWaveform, setAudioWaveform] = useState([]); const [templates, setTemplates] = useState([ { id: "vocab-template", name: "Vocabulary Template", description: "Standard vocabulary template with word and phrases", thumbnail: "template1.jpg", duration: 15, background: "#1a1a2e" }, { id: "grammar-template", name: "Grammar Template", description: "Template for grammar explanations with examples", thumbnail: "template2.jpg", duration: 20, background: "#16213e" }, { id: "conversation-template", name: "Conversation Template", description: "Dialog-based template for conversation practice", thumbnail: "template3.jpg", duration: 30, background: "#0f3460" } ]); const [timelineElements, setTimelineElements] = useState([ { id: "el-1", type: "text", content: "Vocabulary Word", start: 0, duration: 3, position: {x: 400, y: 150}, style: {color: "#ffffff", fontSize: 36, fontWeight: "bold"}, animation: "fadeIn", contentKey: "targetWord" }, { id: "el-2", type: "image", content: "medico.jpg", start: 0.5, duration: 12, position: {x: 200, y: 250}, style: {width: 300, height: 300, borderRadius: "8px"}, animation: "zoomIn", contentKey: "image" }, { id: "el-3", type: "text", content: "Example Phrase 1", start: 4, duration: 3, position: {x: 400, y: 350}, style: {color: "#ffffff", fontSize: 24}, animation: "slideIn", contentKey: "targetPhrase1" }, { id: "el-4", type: "text", content: "Example Phrase 2", start: 8, duration: 3, position: {x: 400, y: 450}, style: {color: "#ffffff", fontSize: 24}, animation: "slideIn", contentKey: "targetPhrase2" }, { id: "el-5", type: "audio", content: "narration.mp3", start: 0, duration: 15, contentKey: "audio" }, ]); // Initialize local storage and load saved projects useEffect(() => { const loadSavedProjects = () => { try { const projectsJson = localStorage.getItem('videogen-projects'); if (projectsJson) { const projects = JSON.parse(projectsJson); setSavedProjects(projects); } } catch (error) { console.error("Error loading saved projects:", error); showNotify("Error loading saved projects", "error"); } }; loadSavedProjects(); // Setup auto-save const autoSaveInterval = setInterval(() => { if (currentTemplate && content.length > 0) { saveCurrentProject(true); } }, 60000); // Auto-save every minute return () => clearInterval(autoSaveInterval); }, []); // Save current project const saveCurrentProject = (isAutoSave = false) => { try { const project = { id: Date.now().toString(), name: projectName, template: currentTemplate, content: content, timelineElements: timelineElements, lastSaved: new Date().toISOString() }; let projects = []; const projectsJson = localStorage.getItem('videogen-projects'); if (projectsJson) { projects = JSON.parse(projectsJson); // Check if project with same name exists const existingProjectIndex = projects.findIndex(p => p.name === projectName); if (existingProjectIndex >= 0) { projects[existingProjectIndex] = project; } else { projects.push(project); } } else { projects = [project]; } localStorage.setItem('videogen-projects', JSON.stringify(projects)); setSavedProjects(projects); if (!isAutoSave) { showNotify("Project saved successfully"); } } catch (error) { console.error("Error saving project:", error); showNotify("Error saving project", "error"); } }; // Load a saved project const loadProject = (project) => { setProjectName(project.name); setCurrentTemplate(project.template); setContent(project.content); setTimelineElements(project.timelineElements); setActiveTab("content"); setShowLoadProjectDialog(false); showNotify(`Project "${project.name}" loaded successfully`); }; // Template selection const selectTemplate = (templateId) => { setCurrentTemplate(templates.find(t => t.id === templateId)); showNotify("Template selected: " + templates.find(t => t.id === templateId).name); setActiveTab("content"); }; // Notification helper const showNotify = (message, type = "success") => { setNotificationMessage(message); setNotificationType(type); setShowNotification(true); setTimeout(() => setShowNotification(false), 3000); }; // Add new content row const addContentRow = () => { if (!newRow.targetWord || !newRow.nativeWord) { showNotify("Target and native words are required", "error"); return; } setContent([...content, { id: content.length + 1, ...newRow, audio: `audio-${content.length + 1}.mp3` // Placeholder for generated audio }]); setNewRow({ image: "", targetWord: "", nativeWord: "", targetPhrase1: "", nativePhrase1: "", targetPhrase2: "", nativePhrase2: "" }); showNotify("New content row added"); }; // Delete content row const deleteContentRow = (id) => { setContent(content.filter(row => row.id !== id)); showNotify("Content row deleted"); }; // Toggle play/pause const togglePlayback = () => { setIsPlaying(!isPlaying); }; // Add new element to timeline const addTimelineElement = (type) => { const newElement = { id: `el-${Date.now()}`, type, content: type === 'text' ? 'New Text' : type === 'image' ? 'image.jpg' : 'audio.mp3', start: currentTime, duration: type === 'audio' ? 5 : 3, position: type !== 'audio' ? {x: 400, y: 300} : undefined, style: type === 'text' ? {color: "#ffffff", fontSize: 24} : type === 'image' ? {width: 200, height: 200, borderRadius: "8px"} : undefined, animation: type !== 'audio' ? selectedAnimationStyle : undefined }; setTimelineElements([...timelineElements, newElement]); setEditingElement(newElement); setShowEditor(true); showNotify(`New ${type} element added`); }; // Animation preview useEffect(() => { let interval; if (isPlaying) { interval = setInterval(() => { setCurrentTime(time => { const newTime = time + 0.1; if (newTime >= duration) { setIsPlaying(false); return 0; } return newTime; }); setProgress(prog => { const newProg = prog + (100 / (duration * 10)); return newProg > 100 ? 0 : newProg; }); }, 100); } return () => clearInterval(interval); }, [isPlaying, duration]); // Handle timeline scrubbing const handleTimelineClick = (e) => { if (!timelineRef.current) return; const rect = timelineRef.current.getBoundingClientRect(); const clickPosition = e.clientX - rect.left; const percentClicked = clickPosition / rect.width; const newTime = percentClicked * duration; setCurrentTime(Math.max(0, Math.min(newTime, duration))); setProgress(percentClicked * 100); }; // Handle timeline drag const handleTimelineDragStart = () => { setIsDraggingTimeline(true); setIsPlaying(false); }; const handleTimelineDragMove = (e) => { if (!isDraggingTimeline || !timelineRef.current) return; const rect = timelineRef.current.getBoundingClientRect(); const dragPosition = e.clientX - rect.left; const percentDragged = Math.max(0, Math.min(dragPosition / rect.width, 1)); const newTime = percentDragged * duration; setCurrentTime(newTime); setProgress(percentDragged * 100); }; const handleTimelineDragEnd = () => { setIsDraggingTimeline(false); }; // Effect for handling window mouse events during timeline dragging useEffect(() => { const handleMouseMove = (e) => handleTimelineDragMove(e); const handleMouseUp = () => handleTimelineDragEnd(); if (isDraggingTimeline) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); } return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDraggingTimeline]); // Generate videos for all content const generateAllVideos = () => { if (!currentTemplate) { showNotify("Please select a template first", "error"); return; } if (content.length === 0) { showNotify("No content to generate", "error"); return; } // Create render queue const queue = content.map(item => ({ id: `render-${item.id}`, content: item, template: currentTemplate, status: "pending", progress: 0 })); setRenderQueue(queue); setGeneratingStatus("preparing"); // Simulate processing setTimeout(() => { setGeneratingStatus("processing"); let currentIndex = 0; const processNext = () => { if (currentIndex >= queue.length) { setGeneratingStatus("complete"); showNotify(`Successfully generated ${queue.length} videos`); return; } const updatedQueue = [...queue]; updatedQueue[currentIndex].status = "processing"; setRenderQueue(updatedQueue); // Simulate processing time (in real app, this would be actual rendering) let progress = 0; const interval = setInterval(() => { progress += 5; const newQueue = [...updatedQueue]; newQueue[currentIndex].progress = progress; setRenderQueue(newQueue); if (progress >= 100) { clearInterval(interval); newQueue[currentIndex].status = "complete"; setRenderQueue(newQueue); currentIndex++; setTimeout(processNext, 500); } }, 300); }; processNext(); }, 1500); }; // Open element editor const openElementEditor = (element) => { setEditingElement(element); setShowEditor(true); }; // Update element const updateElement = (updatedElement) => { setTimelineElements(elements => elements.map(el => el.id === updatedElement.id ? updatedElement : el) ); setShowEditor(false); showNotify("Element updated"); }; // Handle element drag in preview const startElementDrag = (element) => { setDraggedElement(element); }; const handleElementDrag = (e, element) => { if (!previewContainerRef.current) return; const rect = previewContainerRef.current.getBoundingClientRect(); const newX = e.clientX - rect.left; const newY = e.clientY - rect.top; setTimelineElements(elements => elements.map(el => { if (el.id === element.id && el.position) { return { ...el, position: { x: Math.max(0, Math.min(newX, rect.width)), y: Math.max(0, Math.min(newY, rect.height)) } }; } return el; }) ); }; const endElementDrag = () => { setDraggedElement(null); }; // Effect for mouse events during element dragging useEffect(() => { const handleMouseMove = (e) => { if (draggedElement) { handleElementDrag(e, draggedElement); } }; const handleMouseUp = () => { if (draggedElement) { endElementDrag(); } }; if (draggedElement) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); } return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [draggedElement]); // Generate audio for content const generateAudio = () => { showNotify("Generating audio for content..."); // Simulate audio generation let generatedCount = 0; const processNext = () => { if (generatedCount >= content.length) { showNotify("Audio generation complete!"); // Generate mock waveform data generateMockWaveform(); return; } setTimeout(() => { generatedCount++; processNext(); }, 500); }; processNext(); }; // Generate mock waveform for audio visualization const generateMockWaveform = () => { const waveformPoints = []; const numPoints = 100; for (let i = 0; i < numPoints; i++) { // Create a semi-random waveform height, with more activity in the middle const x = i / numPoints; let height = 0.2 + 0.6 * Math.sin(x * Math.PI) * Math.random(); waveformPoints.push(height); } setAudioWaveform(waveformPoints); }; // Export current video const exportCurrentVideo = () => { if (currentContentIndex >= content.length) { showNotify("No content selected for export", "error"); return; } setIsExporting(true); setExportProgress(0); // Simulate export process const interval = setInterval(() => { setExportProgress(prev => { const next = prev + 5; if (next >= 100) { clearInterval(interval); setTimeout(() => { setIsExporting(false); showNotify(`Video exported successfully as ${projectName}-${currentContentIndex + 1}.${exportFormat}`); }, 500); return 100; } return next; }); }, 200); }; // Reset progress for new render const startNewProject = () => { setCurrentTemplate(null); setActiveTab("templates"); setProjectName("New Project"); setContent([]); setRenderQueue([]); setGeneratingStatus(null); setCurrentTime(0); setProgress(0); showNotify("Started new project"); }; // Define placeholder thumbnails and assets const getImageUrl = (name) => { if (name === "medico.jpg") { return "/api/placeholder/300/300"; } else if (name === "abogado.jpg") { return "/api/placeholder/300/300"; } else if (name === "template1.jpg" || name === "template2.jpg" || name === "template3.jpg") { return "/api/placeholder/400/220"; } return "/api/placeholder/200/200"; }; // Get real content based on timeline element const getElementContent = (element) => { if (!element.contentKey || currentContentIndex >= content.length) { return element.content; } const contentItem = content[currentContentIndex]; return contentItem[element.contentKey] || element.content; }; // Timeline rendering const renderTimeline = () => { // Find the time range to display const visibleTimeStart = Math.max(0, currentTime - 5); const visibleTimeEnd = Math.min(duration, currentTime + 10); return (
Project Name
{template.description}
No videos in queue yet
Click the "Generate All Videos" button to start generation