This commit is contained in:
ct
2025-06-18 12:26:07 +08:00
parent aeb8fd6000
commit a40d81331c
7 changed files with 125 additions and 56 deletions

26
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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` }}

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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>