Update
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import useMediaStore from '@/stores/MediaStore';
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import EditNavSidebar from './partials/edit-nav-sidebar';
|
||||
import EditSidebar from './partials/edit-sidebar';
|
||||
import EditorCanvas from './partials/editor-canvas';
|
||||
import EditorControls from './partials/editor-controls';
|
||||
import EditorHeader from './partials/editor-header';
|
||||
import TextSidebar from './partials/text-sidebar';
|
||||
import { calculateOptimalMaxWidth, calculateResponsiveWidth } from './utils/layout-constants';
|
||||
|
||||
// Hook to detect if viewport is below minimum width
|
||||
@@ -101,11 +104,13 @@ const useResponsiveDimensions = () => {
|
||||
|
||||
const Editor = () => {
|
||||
const { init } = useMediaStore();
|
||||
|
||||
const { getSetting } = useLocalSettingsStore();
|
||||
const { setSelectedTextElement } = useVideoEditorStore();
|
||||
const emitter = useMitt();
|
||||
|
||||
const [isEditNavSidebarOpen, setIsEditNavSidebarOpen] = useState(false);
|
||||
const [isEditSidebarOpen, setIsEditSidebarOpen] = useState(false);
|
||||
const [isTextSidebarOpen, setIsTextSidebarOpen] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(true); // Video starts muted by default
|
||||
const { maxWidth, responsiveWidth } = useResponsiveDimensions();
|
||||
const isBelowMinWidth = useViewportDetection(320);
|
||||
@@ -114,6 +119,23 @@ const Editor = () => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
// Listen for text element selection
|
||||
useEffect(() => {
|
||||
const handleTextElementSelected = (textElement) => {
|
||||
setSelectedTextElement(textElement);
|
||||
setIsTextSidebarOpen(true);
|
||||
// Close other sidebars when text sidebar opens
|
||||
setIsEditSidebarOpen(false);
|
||||
setIsEditNavSidebarOpen(false);
|
||||
};
|
||||
|
||||
emitter.on('text-element-selected', handleTextElementSelected);
|
||||
|
||||
return () => {
|
||||
emitter.off('text-element-selected', handleTextElementSelected);
|
||||
};
|
||||
}, [emitter, setSelectedTextElement]);
|
||||
|
||||
const handleEditNavClick = () => {
|
||||
setIsEditNavSidebarOpen(!isEditNavSidebarOpen);
|
||||
};
|
||||
@@ -130,6 +152,11 @@ const Editor = () => {
|
||||
setIsEditSidebarOpen(false);
|
||||
};
|
||||
|
||||
const handleTextSidebarClose = () => {
|
||||
setIsTextSidebarOpen(false);
|
||||
setSelectedTextElement(null);
|
||||
};
|
||||
|
||||
// Toggle mute functionality
|
||||
const handleToggleMute = () => {
|
||||
setIsMuted(!isMuted);
|
||||
@@ -139,6 +166,7 @@ const Editor = () => {
|
||||
<div className="relative mx-auto flex min-h-screen flex-col space-y-2 py-4" style={{ width: `${responsiveWidth}px` }}>
|
||||
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleEditClose} />
|
||||
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
|
||||
<TextSidebar isOpen={isTextSidebarOpen} onClose={handleTextSidebarClose} />
|
||||
|
||||
<EditorHeader
|
||||
className="mx-auto"
|
||||
|
||||
@@ -518,13 +518,17 @@ const VideoEditor = ({ width, height }) => {
|
||||
emitter.on('video-play', handlePlay);
|
||||
emitter.on('video-reset', handleReset);
|
||||
emitter.on('video-seek', handleSeek);
|
||||
emitter.on('text-update', ({ elementId, updates }) => {
|
||||
handleElementUpdate(elementId, updates);
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.off('video-play', handlePlay);
|
||||
emitter.off('video-reset', handleReset);
|
||||
emitter.off('video-seek', handleSeek);
|
||||
emitter.off('text-update');
|
||||
};
|
||||
}, [emitter, handlePlay, handleReset, handleSeek]);
|
||||
}, [emitter, handlePlay, handleReset, handleSeek, handleElementUpdate]);
|
||||
|
||||
return (
|
||||
<div style={{ width: dimensions.width, height: dimensions.height }} className="rounded-3xl">
|
||||
|
||||
@@ -98,15 +98,18 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
||||
|
||||
// Process text elements with centering
|
||||
texts.forEach((t, i) => {
|
||||
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:');
|
||||
|
||||
// Center the text: x position is the center point, y is adjusted for baseline
|
||||
const centerX = Math.round(t.x);
|
||||
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
|
||||
|
||||
filters.push(
|
||||
`[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${Math.round(
|
||||
t.x,
|
||||
)}:y=${Math.round(t.y)}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
||||
`[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
||||
t.stroke
|
||||
}:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
|
||||
}:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
|
||||
);
|
||||
videoLayer = `t${i}`;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Image, Layer, Line, Stage, Text, Transformer } from 'react-konva';
|
||||
|
||||
@@ -36,6 +37,8 @@ const VideoPreview = ({
|
||||
// Refs
|
||||
layerRef,
|
||||
}) => {
|
||||
const emitter = useMitt();
|
||||
|
||||
// Selection state
|
||||
const [selectedElementId, setSelectedElementId] = useState(null);
|
||||
const transformerRef = useRef(null);
|
||||
@@ -116,8 +119,18 @@ const VideoPreview = ({
|
||||
};
|
||||
|
||||
// Handle element selection
|
||||
const handleElementSelect = useCallback((elementId) => {
|
||||
const handleElementSelect = useCallback(
|
||||
(elementId) => {
|
||||
setSelectedElementId(elementId);
|
||||
|
||||
// Find the selected element
|
||||
const element = timelineElements.find((el) => el.id === elementId);
|
||||
|
||||
// If it's a text element, emit text-element-selected event
|
||||
if (element && element.type === 'text') {
|
||||
emitter.emit('text-element-selected', element);
|
||||
}
|
||||
|
||||
// Clear guide lines when selecting
|
||||
setGuideLines({
|
||||
vertical: null,
|
||||
@@ -125,7 +138,9 @@ const VideoPreview = ({
|
||||
showVertical: false,
|
||||
showHorizontal: false,
|
||||
});
|
||||
}, []);
|
||||
},
|
||||
[emitter, timelineElements],
|
||||
);
|
||||
|
||||
// Handle clicking on empty space to deselect
|
||||
const handleStageClick = useCallback((e) => {
|
||||
@@ -434,6 +449,10 @@ const VideoPreview = ({
|
||||
stroke={element.stroke}
|
||||
strokeWidth={element.strokeWidth}
|
||||
rotation={element.rotation || 0}
|
||||
// Center the text horizontally
|
||||
align="center"
|
||||
// Let text have natural width and height for multiline support
|
||||
wrap="word"
|
||||
draggable
|
||||
dragBoundFunc={createDragBoundFunc(element.id)}
|
||||
onClick={() => handleElementSelect(element.id)}
|
||||
|
||||
67
resources/js/modules/editor/partials/text-sidebar.jsx
Normal file
67
resources/js/modules/editor/partials/text-sidebar.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import { Type } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function TextSidebar({ isOpen, onClose }) {
|
||||
const { selectedTextElement } = useVideoEditorStore();
|
||||
const emitter = useMitt();
|
||||
const [textValue, setTextValue] = useState('');
|
||||
|
||||
// Update textarea when selected element changes
|
||||
useEffect(() => {
|
||||
if (selectedTextElement) {
|
||||
setTextValue(selectedTextElement.text || '');
|
||||
}
|
||||
}, [selectedTextElement]);
|
||||
|
||||
// Handle text changes
|
||||
const handleTextChange = (e) => {
|
||||
const newText = e.target.value;
|
||||
setTextValue(newText);
|
||||
|
||||
if (selectedTextElement) {
|
||||
emitter.emit('text-update', {
|
||||
elementId: selectedTextElement.id,
|
||||
updates: { text: newText },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-3">
|
||||
<Type className="h-6 w-6" />
|
||||
Edit Text
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mt-6 space-y-4 px-2">
|
||||
{selectedTextElement ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="text-sm font-medium">Edit your text here</label>
|
||||
<Textarea
|
||||
value={textValue}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Enter your text..."
|
||||
className="mt-2 text-center"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-500">
|
||||
<Type className="mx-auto h-12 w-12 text-gray-300" />
|
||||
<p className="mt-2">Select a text element to edit</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import axiosInstance from '@/plugins/axios-plugin';
|
||||
import { mountStoreDevtool } from 'simple-zustand-devtools';
|
||||
import { toast } from 'sonner';
|
||||
import { route } from 'ziggy-js';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
const useVideoEditorStore = create(
|
||||
devtools((set, get) => ({
|
||||
videoIsPlaying: false,
|
||||
selectedTextElement: null,
|
||||
|
||||
setVideoIsPlaying: (isPlaying) => {
|
||||
set({ videoIsPlaying: isPlaying });
|
||||
},
|
||||
|
||||
setSelectedTextElement: (element) => {
|
||||
set({ selectedTextElement: element });
|
||||
},
|
||||
})),
|
||||
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user