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-error-boundary": "^6.0.0",
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"react-konva": "^19.0.6", "react-konva": "^19.0.6",
"react-konva-utils": "^1.1.0",
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"simple-zustand-devtools": "^1.1.0", "simple-zustand-devtools": "^1.1.0",
@@ -7274,6 +7275,21 @@
"react-dom": "^18.3.1 || ^19.0.0" "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": { "node_modules/react-reconciler": {
"version": "0.32.0", "version": "0.32.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz", "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": { "node_modules/use-sidecar": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "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-error-boundary": "^6.0.0",
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"react-konva": "^19.0.6", "react-konva": "^19.0.6",
"react-konva-utils": "^1.1.0",
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"simple-zustand-devtools": "^1.1.0", "simple-zustand-devtools": "^1.1.0",

View File

@@ -119,14 +119,11 @@ const Editor = () => {
init(); init();
}, []); }, []);
// Listen for text element selection // Listen for text element selection (but don't auto-open sidebar)
useEffect(() => { useEffect(() => {
const handleTextElementSelected = (textElement) => { const handleTextElementSelected = (textElement) => {
setSelectedTextElement(textElement); setSelectedTextElement(textElement);
setIsTextSidebarOpen(true); // Remove automatic sidebar opening - user will click the button instead
// Close other sidebars when text sidebar opens
setIsEditSidebarOpen(false);
setIsEditNavSidebarOpen(false);
}; };
emitter.on('text-element-selected', handleTextElementSelected); emitter.on('text-element-selected', handleTextElementSelected);
@@ -152,6 +149,13 @@ const Editor = () => {
setIsEditSidebarOpen(false); setIsEditSidebarOpen(false);
}; };
const handleTextSidebarOpen = () => {
setIsTextSidebarOpen(true);
// Close other sidebars when text sidebar opens
setIsEditSidebarOpen(false);
setIsEditNavSidebarOpen(false);
};
const handleTextSidebarClose = () => { const handleTextSidebarClose = () => {
setIsTextSidebarOpen(false); setIsTextSidebarOpen(false);
setSelectedTextElement(null); setSelectedTextElement(null);
@@ -197,7 +201,7 @@ const Editor = () => {
</div> </div>
) : ( ) : (
<> <>
<EditorCanvas maxWidth={maxWidth} /> <EditorCanvas maxWidth={maxWidth} onOpenTextSidebar={handleTextSidebarOpen} />
<EditorControls <EditorControls
className="mx-auto" className="mx-auto"
style={{ width: `${responsiveWidth}px` }} style={{ width: `${responsiveWidth}px` }}

View File

@@ -7,7 +7,7 @@ import { generateTimelineFromTemplate } from '../../utils/timeline-template-proc
import useVideoExport from './video-export'; import useVideoExport from './video-export';
import VideoPreview from './video-preview'; import VideoPreview from './video-preview';
const VideoEditor = ({ width, height }) => { const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
const [showConsoleLogs] = useState(true); const [showConsoleLogs] = useState(true);
const [dimensions] = useState({ const [dimensions] = useState({
@@ -586,6 +586,7 @@ const VideoEditor = ({ width, height }) => {
copyFFmpegCommand={copyFFmpegCommand} copyFFmpegCommand={copyFFmpegCommand}
exportVideo={exportVideo} exportVideo={exportVideo}
onElementUpdate={handleElementUpdate} onElementUpdate={handleElementUpdate}
onOpenTextSidebar={onOpenTextSidebar}
layerRef={layerRef} layerRef={layerRef}
/> />
</div> </div>

View File

@@ -1,6 +1,10 @@
import { Button } from '@/components/ui/button';
import { useMitt } from '@/plugins/MittContext'; import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore';
import { Type } from 'lucide-react';
import { useEffect, useRef } from '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 our custom hooks and utilities
import { useElementSelection } from './video-preview/video-preview-element-selection'; import { useElementSelection } from './video-preview/video-preview-element-selection';
@@ -38,11 +42,13 @@ const VideoPreview = ({
copyFFmpegCommand, copyFFmpegCommand,
exportVideo, exportVideo,
onElementUpdate, // New prop for updating element properties onElementUpdate, // New prop for updating element properties
onOpenTextSidebar, // New prop for opening text sidebar
// Refs // Refs
layerRef, layerRef,
}) => { }) => {
const emitter = useMitt(); const emitter = useMitt();
const { selectedTextElement } = useVideoEditorStore();
// Refs for elements and transformer // Refs for elements and transformer
const transformerRef = useRef(null); const transformerRef = useRef(null);
@@ -87,7 +93,7 @@ const VideoPreview = ({
}, [selectedElementId, activeElements]); }, [selectedElementId, activeElements]);
return ( return (
<div> <div className="relative">
<Stage width={dimensions.width} height={dimensions.height} ref={stageRef} onClick={handleStageClick} onTap={handleStageClick}> <Stage width={dimensions.width} height={dimensions.height} ref={stageRef} onClick={handleStageClick} onTap={handleStageClick}>
<Layer ref={layerRef}> <Layer ref={layerRef}>
{activeElements.map((element) => { {activeElements.map((element) => {
@@ -133,8 +139,8 @@ const VideoPreview = ({
); );
} else if (element.type === 'text') { } else if (element.type === 'text') {
return ( return (
<Group key={element.id}>
<Text <Text
key={element.id}
ref={(node) => { ref={(node) => {
if (node) { if (node) {
elementRefs.current[element.id] = node; elementRefs.current[element.id] = node;
@@ -172,6 +178,33 @@ const VideoPreview = ({
offsetX={element.offsetX} offsetX={element.offsetX}
// Visual feedback for selection // 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) { } else if (element.type === 'image' && element.imageElement && element.isImageReady) {
return ( return (

View File

@@ -51,7 +51,7 @@ const useResponsiveCanvas = (maxWidth = 350) => {
return scale; return scale;
}; };
const EditorCanvas = ({ maxWidth = 350 }) => { const EditorCanvas = ({ maxWidth = 350, onOpenTextSidebar }) => {
const scale = useResponsiveCanvas(maxWidth); const scale = useResponsiveCanvas(maxWidth);
const displayWidth = LAYOUT_CONSTANTS.CANVAS_WIDTH * scale; const displayWidth = LAYOUT_CONSTANTS.CANVAS_WIDTH * scale;
const displayHeight = LAYOUT_CONSTANTS.CANVAS_HEIGHT * scale; const displayHeight = LAYOUT_CONSTANTS.CANVAS_HEIGHT * scale;
@@ -86,7 +86,11 @@ const EditorCanvas = ({ maxWidth = 350 }) => {
console.log(`Canvas coordinates: x=${x}, y=${y}`); 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> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useMitt } from '@/plugins/MittContext'; import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore'; 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 EditorControls = ({ className = '', onEditClick = () => {}, isEditActive = false }) => {
const { videoIsPlaying } = useVideoEditorStore(); const { videoIsPlaying } = useVideoEditorStore();
@@ -28,7 +28,7 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
return ( return (
<div className={cn('flex items-center justify-center gap-2', className)}> <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" />} {videoIsPlaying ? <Square className="h-8 w-8" /> : <Play className="h-8 w-8" />}
</Button> </Button>
@@ -40,13 +40,13 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
<span className="text-sm font-medium ">9:16</span> <span className="text-sm font-medium ">9:16</span>
</Button> */} </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" /> <Type className="h-8 w-8" />
</Button> </Button> */}
<Button <Button
id="edit" id="edit"
variant={isEditActive ? 'default' : 'ghost'} variant={isEditActive ? 'default' : 'default'}
size="icon" size="icon"
className="h-12 w-12 rounded-full border shadow-sm" className="h-12 w-12 rounded-full border shadow-sm"
onClick={onEditClick} onClick={onEditClick}
@@ -54,7 +54,7 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
<Edit3 className={`h-8 w-8 ${isEditActive ? 'text-white' : ''}`} /> <Edit3 className={`h-8 w-8 ${isEditActive ? 'text-white' : ''}`} />
</Button> </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" /> <Download className="h-8 w-8" />
</Button> </Button>
</div> </div>