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();
}
};
// 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 && (
)}
{/* 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;