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

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>