import { Button } from '@/components/ui/button'; import { useMitt } from '@/plugins/MittContext'; import useVideoEditorStore from '@/stores/VideoEditorStore'; import { Type } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { Group, Image, Layer, Line, Stage, Text, Transformer } from 'react-konva'; import { Html } from 'react-konva-utils'; // Import our custom hooks and utilities import { useElementSelection } from './video-preview/video-preview-element-selection'; import { useElementTransform } from './video-preview/video-preview-element-transform'; import { getImageSource } from './video-preview/video-preview-utils'; // Import centralized font management import { getFontStyle, loadTimelineFonts, WATERMARK_CONFIG } from '@/modules/editor/fonts'; const VideoPreview = ({ watermarked, // Dimensions dimensions, // Timeline state currentTime, totalDuration, isPlaying, status, // Export state isExporting, exportProgress, exportStatus, // Data timelineElements, activeElements, videoElements, loadedVideos, videoStates, ffmpegCommand, // Event handlers handlePlay, handlePause, handleReset, handleSeek, copyFFmpegCommand, exportVideo, onElementUpdate, // New prop for updating element properties onOpenTextSidebar, // New prop for opening text sidebar // Refs layerRef, }) => { const emitter = useMitt(); const { selectedTextElement } = useVideoEditorStore(); // Refs for elements and transformer const transformerRef = useRef(null); const stageRef = useRef(null); const elementRefs = useRef({}); // Font loading state const [fontsLoaded, setFontsLoaded] = useState(false); const [fontLoadingAttempted, setFontLoadingAttempted] = useState(false); // Load fonts when timeline elements change useEffect(() => { const loadFonts = async () => { if (timelineElements.length > 0 && !fontLoadingAttempted) { setFontLoadingAttempted(true); try { await loadTimelineFonts(timelineElements); setFontsLoaded(true); console.log('✅ Fonts loaded in preview'); // Force redraw after fonts load to recalculate text dimensions setTimeout(() => { if (layerRef.current) { layerRef.current.batchDraw(); } }, 100); } catch (error) { console.warn('⚠️ Font loading failed:', error); setFontsLoaded(true); // Continue anyway with fallback fonts } } }; loadFonts(); }, [timelineElements, fontLoadingAttempted]); // Force text remeasurement when fonts load useEffect(() => { if (fontsLoaded && layerRef.current) { // Find all text nodes and force them to recalculate const textNodes = layerRef.current.find('Text'); textNodes.forEach((textNode) => { // Force Konva to recalculate text dimensions textNode._setTextData(); textNode.cache(); textNode.clearCache(); }); layerRef.current.batchDraw(); } }, [fontsLoaded]); // Use our custom hooks const { selectedElementId, setSelectedElementId, handleElementSelect, handleStageClick: baseHandleStageClick, } = useElementSelection(emitter, timelineElements); const { guideLines, handleDragMove, handleDragEnd, createDragBoundFunc, handleTransform, clearGuideLines } = useElementTransform( timelineElements, dimensions, onElementUpdate, elementRefs, ); // Enhanced stage click handler that also clears guide lines const handleStageClick = (e) => { baseHandleStageClick(e); if (e.target === e.target.getStage()) { clearGuideLines(); } }; // Pre-select first text node on load useEffect(() => { if (activeElements.length > 0 && !selectedElementId) { const firstTextElement = activeElements.find((element) => element.type === 'text'); if (firstTextElement) { setSelectedElementId(firstTextElement.id); } } }, [activeElements, selectedElementId, setSelectedElementId]); // Update transformer when selection changes useEffect(() => { if (transformerRef.current) { const selectedNode = selectedElementId ? elementRefs.current[selectedElementId] : null; if (selectedNode) { transformerRef.current.nodes([selectedNode]); transformerRef.current.getLayer().batchDraw(); } else { transformerRef.current.nodes([]); } } }, [selectedElementId, activeElements]); return (
{activeElements.map((element) => { const isSelected = selectedElementId === element.id; if (element.type === 'video') { const imageSource = getImageSource(element, videoStates, isPlaying); if (!imageSource) { return null; } return ( { if (node) { elementRefs.current[element.id] = node; } }} image={imageSource} // Use center position for x,y when offset is set x={element.x + element.width / 2} y={element.y + element.height / 2} width={element.width} height={element.height} // Set offset to center for proper rotation offsetX={element.width / 2} offsetY={element.height / 2} rotation={element.rotation || 0} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Visual feedback for selection stroke={isSelected ? '#0066ff' : undefined} strokeWidth={isSelected ? 2 : 0} strokeScaleEnabled={false} /> ); } else if (element.type === 'text') { return ( { if (node) { elementRefs.current[element.id] = node; // Force text measurement after font loading if (fontsLoaded) { setTimeout(() => { node._setTextData(); // Debug log preview text properties console.log(`🔍 Preview Text ${element.id} Properties:`); console.log(' text:', element.text); console.log(' x:', element.x); console.log(' y:', element.y); console.log(' fontSize:', element.fontSize); console.log(' fontFamily:', element.fontFamily); console.log(' width (fixedWidth):', element.fixedWidth); console.log(' offsetX:', element.offsetX); console.log(' node.width():', node.width()); console.log(' node.height():', node.height()); console.log(' node.textWidth:', node.textWidth); console.log(' node.textHeight:', node.textHeight); if (layerRef.current) { layerRef.current.batchDraw(); } }, 0); } } }} text={element.text} x={element.x} y={element.y} fontSize={element.fontSize} fontStyle={getFontStyle(element)} // Use centralized function fontFamily={element.fontFamily || 'Montserrat'} fill={element.fill || '#ffffff'} stroke={element.strokeWidth > 0 ? element.stroke || '#000000' : undefined} strokeWidth={element.strokeWidth * 3 || 0} fillAfterStrokeEnabled={true} strokeScaleEnabled={false} rotation={element.rotation || 0} // Center the text horizontally align="center" verticalAlign="middle" // Let text have natural width and height for multiline support wrap="word" // Always scale 1 - size changes go through fontSize scaleX={1} scaleY={1} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Apply fixedWidth and offsetX if they exist width={element.fixedWidth} offsetX={element.offsetX} // Visual feedback for selection /> {/* Edit button - only show when this text element is selected */} {isSelected && ( )} ); } else if (element.type === 'image' && element.imageElement && element.isImageReady) { return ( { if (node) { elementRefs.current[element.id] = node; } }} image={element.imageElement} // Use center position for x,y when offset is set x={element.x + element.width / 2} y={element.y + element.height / 2} width={element.width} height={element.height} // Set offset to center for proper rotation offsetX={element.width / 2} offsetY={element.height / 2} rotation={element.rotation || 0} draggable dragBoundFunc={createDragBoundFunc(element.id)} onClick={() => handleElementSelect(element.id)} onTap={() => handleElementSelect(element.id)} onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} // Visual feedback for selection stroke={isSelected ? '#0066ff' : undefined} strokeWidth={isSelected ? 2 : 0} strokeScaleEnabled={false} /> ); } return null; })} {/* Watermark - only show when watermarked is true */} {watermarked && ( { if (node && fontsLoaded) { // Force text measurement after font loading setTimeout(() => { node._setTextData(); // Update offset to properly center the text const textWidth = node.width(); const textHeight = node.height(); node.offsetX(textWidth / 2); node.offsetY(textHeight / 2); if (layerRef.current) { layerRef.current.batchDraw(); } }, 0); } }} text="memefa.st" x={dimensions.width / 2} y={dimensions.height / 2 + dimensions.height * 0.2} fontSize={WATERMARK_CONFIG.fontSize} fontFamily={WATERMARK_CONFIG.fontFamily} fill={WATERMARK_CONFIG.fill} stroke={WATERMARK_CONFIG.stroke} strokeWidth={WATERMARK_CONFIG.strokeWidth} opacity={WATERMARK_CONFIG.opacity} align="center" verticalAlign="middle" draggable={false} listening={false} // Prevents any mouse interactions /> )} {/* Guide Lines Layer */} {guideLines.showVertical && ( )} {guideLines.showHorizontal && ( )} {/* Transformer for selected element */} { // Limit resize to prevent elements from becoming too small if (newBox.width < 20 || newBox.height < 20) { return oldBox; } return newBox; }} // Transformer styling - Figma-like appearance borderStroke="#0066ff" borderStrokeWidth={2} anchorStroke="#0066ff" anchorFill="white" anchorSize={14} anchorCornerRadius={2} // Enable only corner anchors for aspect ratio enabledAnchors={['top-left', 'top-right', 'bottom-right', 'bottom-left']} // Rotation handle rotateAnchorOffset={30} // Built-in Konva rotation snapping rotationSnaps={[0, 90, 180, 270]} rotationSnapTolerance={8} // Clear guide lines when transform ends onTransformEnd={clearGuideLines} // Style the rotation anchor to be circular anchorStyleFunc={(anchor) => { if (anchor.hasName('.rotater')) { // Make it circular by setting corner radius to half the width anchor.cornerRadius(anchor.width() / 2); anchor.fill('#0066ff'); anchor.stroke('white'); anchor.strokeWidth(1); } }} />
); }; export default VideoPreview;