Update
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -60,6 +60,7 @@
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-konva": "^19.0.6",
|
||||
"react-konva-utils": "^1.1.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"recharts": "^2.15.3",
|
||||
"simple-zustand-devtools": "^1.1.0",
|
||||
@@ -7274,6 +7275,21 @@
|
||||
"react-dom": "^18.3.1 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-konva-utils": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-konva-utils/-/react-konva-utils-1.1.0.tgz",
|
||||
"integrity": "sha512-3ylRN4eUeYU553tmY2Hgi69efrlUBjE8MLXXAQT+rLDBCPW4CwlvJFUCgPQoSEZmaJKTH/gvZz0Y4f8tUfV0rw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-image": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"konva": "^8.3.5 || ^9.0.0",
|
||||
"react": "^18.2.0 || ^19.0.0",
|
||||
"react-dom": "^18.2.0 || ^19.0.0",
|
||||
"react-konva": "^18.2.10 || ^19.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz",
|
||||
@@ -8435,6 +8451,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-image": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/use-image/-/use-image-1.1.4.tgz",
|
||||
"integrity": "sha512-P+swhszzHHgEb2X2yQ+vQNPCq/8Ks3hyfdXAVN133pvnvK7UK++bUaZUa5E+A3S02Mw8xOCBr9O6CLhk2fjrWA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-konva": "^19.0.6",
|
||||
"react-konva-utils": "^1.1.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"recharts": "^2.15.3",
|
||||
"simple-zustand-devtools": "^1.1.0",
|
||||
|
||||
@@ -119,14 +119,11 @@ const Editor = () => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
// Listen for text element selection
|
||||
// Listen for text element selection (but don't auto-open sidebar)
|
||||
useEffect(() => {
|
||||
const handleTextElementSelected = (textElement) => {
|
||||
setSelectedTextElement(textElement);
|
||||
setIsTextSidebarOpen(true);
|
||||
// Close other sidebars when text sidebar opens
|
||||
setIsEditSidebarOpen(false);
|
||||
setIsEditNavSidebarOpen(false);
|
||||
// Remove automatic sidebar opening - user will click the button instead
|
||||
};
|
||||
|
||||
emitter.on('text-element-selected', handleTextElementSelected);
|
||||
@@ -152,6 +149,13 @@ const Editor = () => {
|
||||
setIsEditSidebarOpen(false);
|
||||
};
|
||||
|
||||
const handleTextSidebarOpen = () => {
|
||||
setIsTextSidebarOpen(true);
|
||||
// Close other sidebars when text sidebar opens
|
||||
setIsEditSidebarOpen(false);
|
||||
setIsEditNavSidebarOpen(false);
|
||||
};
|
||||
|
||||
const handleTextSidebarClose = () => {
|
||||
setIsTextSidebarOpen(false);
|
||||
setSelectedTextElement(null);
|
||||
@@ -197,7 +201,7 @@ const Editor = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<EditorCanvas maxWidth={maxWidth} />
|
||||
<EditorCanvas maxWidth={maxWidth} onOpenTextSidebar={handleTextSidebarOpen} />
|
||||
<EditorControls
|
||||
className="mx-auto"
|
||||
style={{ width: `${responsiveWidth}px` }}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generateTimelineFromTemplate } from '../../utils/timeline-template-proc
|
||||
import useVideoExport from './video-export';
|
||||
import VideoPreview from './video-preview';
|
||||
|
||||
const VideoEditor = ({ width, height }) => {
|
||||
const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
const [showConsoleLogs] = useState(true);
|
||||
|
||||
const [dimensions] = useState({
|
||||
@@ -586,6 +586,7 @@ const VideoEditor = ({ width, height }) => {
|
||||
copyFFmpegCommand={copyFFmpegCommand}
|
||||
exportVideo={exportVideo}
|
||||
onElementUpdate={handleElementUpdate}
|
||||
onOpenTextSidebar={onOpenTextSidebar}
|
||||
layerRef={layerRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import { Type } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Image, Layer, Line, Stage, Text, Transformer } from 'react-konva';
|
||||
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';
|
||||
@@ -38,11 +42,13 @@ const VideoPreview = ({
|
||||
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);
|
||||
@@ -87,7 +93,7 @@ const VideoPreview = ({
|
||||
}, [selectedElementId, activeElements]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative">
|
||||
<Stage width={dimensions.width} height={dimensions.height} ref={stageRef} onClick={handleStageClick} onTap={handleStageClick}>
|
||||
<Layer ref={layerRef}>
|
||||
{activeElements.map((element) => {
|
||||
@@ -133,45 +139,72 @@ const VideoPreview = ({
|
||||
);
|
||||
} else if (element.type === 'text') {
|
||||
return (
|
||||
<Text
|
||||
key={element.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
elementRefs.current[element.id] = node;
|
||||
}
|
||||
}}
|
||||
text={element.text}
|
||||
x={element.x}
|
||||
y={element.y}
|
||||
fontSize={element.fontSize}
|
||||
fontStyle={getTextFontStyle(element)}
|
||||
fontFamily={element.fontFamily || 'Arial'}
|
||||
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
|
||||
/>
|
||||
<Group key={element.id}>
|
||||
<Text
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
elementRefs.current[element.id] = node;
|
||||
}
|
||||
}}
|
||||
text={element.text}
|
||||
x={element.x}
|
||||
y={element.y}
|
||||
fontSize={element.fontSize}
|
||||
fontStyle={getTextFontStyle(element)}
|
||||
fontFamily={element.fontFamily || 'Arial'}
|
||||
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 && (
|
||||
<Html
|
||||
groupProps={{
|
||||
x: element.x - (element.offsetX || 0),
|
||||
y: element.y - 50,
|
||||
}}
|
||||
divProps={{
|
||||
style: {
|
||||
zIndex: 10,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-full border shadow-sm"
|
||||
onClick={() => {
|
||||
handleElementSelect(element.id);
|
||||
onOpenTextSidebar();
|
||||
}}
|
||||
>
|
||||
<Type className="h-8 w-8" />
|
||||
</Button>
|
||||
</Html>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
} else if (element.type === 'image' && element.imageElement && element.isImageReady) {
|
||||
return (
|
||||
|
||||
@@ -51,7 +51,7 @@ const useResponsiveCanvas = (maxWidth = 350) => {
|
||||
return scale;
|
||||
};
|
||||
|
||||
const EditorCanvas = ({ maxWidth = 350 }) => {
|
||||
const EditorCanvas = ({ maxWidth = 350, onOpenTextSidebar }) => {
|
||||
const scale = useResponsiveCanvas(maxWidth);
|
||||
const displayWidth = LAYOUT_CONSTANTS.CANVAS_WIDTH * scale;
|
||||
const displayHeight = LAYOUT_CONSTANTS.CANVAS_HEIGHT * scale;
|
||||
@@ -86,7 +86,11 @@ const EditorCanvas = ({ maxWidth = 350 }) => {
|
||||
console.log(`Canvas coordinates: x=${x}, y=${y}`);
|
||||
}}
|
||||
>
|
||||
<VideoEditor width={LAYOUT_CONSTANTS.CANVAS_WIDTH} height={LAYOUT_CONSTANTS.CANVAS_HEIGHT} />
|
||||
<VideoEditor
|
||||
width={LAYOUT_CONSTANTS.CANVAS_WIDTH}
|
||||
height={LAYOUT_CONSTANTS.CANVAS_HEIGHT}
|
||||
onOpenTextSidebar={onOpenTextSidebar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import { Download, Edit3, Play, Square, Type } from 'lucide-react';
|
||||
import { Download, Edit3, Play, Square } from 'lucide-react';
|
||||
|
||||
const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive = false }) => {
|
||||
const { videoIsPlaying } = useVideoEditorStore();
|
||||
@@ -28,7 +28,7 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center gap-2', className)}>
|
||||
<Button onClick={togglePlayPause} variant="ghost" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Button onClick={togglePlayPause} variant="default" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
{videoIsPlaying ? <Square className="h-8 w-8" /> : <Play className="h-8 w-8" />}
|
||||
</Button>
|
||||
|
||||
@@ -40,13 +40,13 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
|
||||
<span className="text-sm font-medium ">9:16</span>
|
||||
</Button> */}
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
{/* <Button variant="ghost" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Type className="h-8 w-8" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
<Button
|
||||
id="edit"
|
||||
variant={isEditActive ? 'default' : 'ghost'}
|
||||
variant={isEditActive ? 'default' : 'default'}
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-full border shadow-sm"
|
||||
onClick={onEditClick}
|
||||
@@ -54,7 +54,7 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
|
||||
<Edit3 className={`h-8 w-8 ${isEditActive ? 'text-white' : ''}`} />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Button variant="default" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Download className="h-8 w-8" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user