From a010059ef1ed338076b7753fe0fe4098b1caf8b5 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 28 Dec 2025 12:16:35 +0330 Subject: [PATCH] add video shape --- src/components/VideoUploadZone.tsx | 36 +++ .../sidebar/instructions/VideoInput.tsx | 112 ++++++--- .../editor/components/tools/VideoShape.tsx | 215 ++++++++++++++---- 3 files changed, 285 insertions(+), 78 deletions(-) create mode 100644 src/components/VideoUploadZone.tsx diff --git a/src/components/VideoUploadZone.tsx b/src/components/VideoUploadZone.tsx new file mode 100644 index 0000000..024a3b9 --- /dev/null +++ b/src/components/VideoUploadZone.tsx @@ -0,0 +1,36 @@ +import { type FC } from "react"; +import { DocumentUpload, VideoSquare } from "iconsax-react"; + +type VideoUploadZoneProps = { + getRootProps: () => React.HTMLAttributes; + getInputProps: () => React.InputHTMLAttributes; +}; + +const VideoUploadZone: FC = ({ getRootProps, getInputProps }) => { + return ( +
+ + +
+ ویدیو مورد نظر را آپلود کنید +
+
+ +
آپلود
+
+
+ ); +}; + +export default VideoUploadZone; + diff --git a/src/pages/editor/components/sidebar/instructions/VideoInput.tsx b/src/pages/editor/components/sidebar/instructions/VideoInput.tsx index fecbc10..66b63e8 100644 --- a/src/pages/editor/components/sidebar/instructions/VideoInput.tsx +++ b/src/pages/editor/components/sidebar/instructions/VideoInput.tsx @@ -1,37 +1,89 @@ +import { type FC, useCallback, useMemo } from "react"; import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore"; -import Input from "@/components/Input"; +import { CloseCircle } from "iconsax-react"; +import { useDropzone } from "react-dropzone"; +import VideoUploadZone from "@/components/VideoUploadZone"; + +const VideoInput: FC = () => { + const { objects, addObject, setSelectedObjectId, setTool, deleteObject } = useEditorStore(); + + const onDrop = useCallback((acceptedFiles: File[]) => { + acceptedFiles.forEach((file) => { + const videoUrl = URL.createObjectURL(file); + const videoId = `video-${Date.now()}-${Math.random()}`; + + + + const newVideo = { + id: videoId, + type: "video" as ToolType, + x: 100, + y: 100, + width: 400, + height: 300, + videoUrl, + }; + + addObject(newVideo); + setSelectedObjectId(newVideo.id); + setTool("select"); + }); + }, [addObject, setSelectedObjectId, setTool]); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: { + 'video/*': ['.mp4', '.webm', '.ogg', '.mov', '.avi'] + }, + multiple: true + }); + + const videoObjects = useMemo(() => { + return objects.filter((obj) => obj.type === "video"); + }, [objects]); + + const handleDeleteVideo = (videoId: string) => { + const videoObject = videoObjects.find((v) => v.id === videoId); + if (videoObject?.videoUrl) { + URL.revokeObjectURL(videoObject.videoUrl); + } + deleteObject(videoId); + }; -const VideoInput = () => { return (
-
- { - const input = document.querySelector('input[placeholder="https://example.com/video.mp4"]') as HTMLInputElement; - if (input?.value) { - const newVideo = { - id: `video-${Date.now()}`, - type: "video" as ToolType, - x: 100, - y: 100, - width: 400, - height: 300, - videoUrl: input.value, - }; - const { addObject, setSelectedObjectId, setTool } = useEditorStore.getState(); - addObject(newVideo); - setSelectedObjectId(newVideo.id); - setTool("select"); - input.value = ""; - } - }} - /> -
-
-

آدرس ویدیو را وارد کنید

-
+
ویدیو
+ + + + {videoObjects.length > 0 && ( +
+
ویدیو های آپلود شده
+
+ {videoObjects.map((videoObject) => ( +
+
+ ))} +
+
+ )}
); }; diff --git a/src/pages/editor/components/tools/VideoShape.tsx b/src/pages/editor/components/tools/VideoShape.tsx index e94751a..6157a76 100644 --- a/src/pages/editor/components/tools/VideoShape.tsx +++ b/src/pages/editor/components/tools/VideoShape.tsx @@ -1,7 +1,9 @@ -import { useEffect, useRef } from "react"; -import { Rect, Text } from "react-konva"; +import { useEffect, useRef, useState } from "react"; +import { Image as KonvaImage, Rect, Group, Circle, Text as KonvaText } from "react-konva"; import Konva from "konva"; +import useImage from "use-image"; import type { ShapeProps } from "./types"; +import { createPortal } from "react-dom"; const VideoShape = ({ obj, @@ -11,33 +13,112 @@ const VideoShape = ({ onUpdate, draggable, }: ShapeProps) => { - const shapeRef = useRef(null); + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [showVideoModal, setShowVideoModal] = useState(false); + const groupRef = useRef(null); + const [image] = useImage(thumbnailUrl || ""); useEffect(() => { - if (isSelected && shapeRef.current && transformerRef.current) { - transformerRef.current.nodes([shapeRef.current]); + if (!obj.videoUrl) return; + + const video = document.createElement("video"); + video.src = obj.videoUrl; + video.crossOrigin = "anonymous"; + video.muted = true; + + const handleLoadedMetadata = () => { + video.currentTime = 0.1; + }; + + const handleSeeked = () => { + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth || 400; + canvas.height = video.videoHeight || 300; + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnail = canvas.toDataURL("image/png"); + setThumbnailUrl(thumbnail); + } + video.removeEventListener("seeked", handleSeeked); + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + }; + + video.addEventListener("loadedmetadata", handleLoadedMetadata); + video.addEventListener("seeked", handleSeeked); + video.load(); + + return () => { + video.removeEventListener("seeked", handleSeeked); + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + }; + }, [obj.videoUrl]); + + useEffect(() => { + if (isSelected && groupRef.current && transformerRef.current && image) { + transformerRef.current.nodes([groupRef.current]); transformerRef.current.getLayer()?.batchDraw(); } - }, [isSelected, transformerRef]); + }, [isSelected, transformerRef, image]); + + useEffect(() => { + if (!showVideoModal) return; + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setShowVideoModal(false); + } + }; + + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [showVideoModal]); + + const containerWidth = obj.width || 400; + const containerHeight = obj.height || 300; + + const handlePlayClick = (e: Konva.KonvaEventObject) => { + e.cancelBubble = true; + const evt = e.evt; + if (evt) { + evt.stopPropagation(); + evt.stopImmediatePropagation(); + evt.preventDefault(); + evt.cancelBubble = true; + } + requestAnimationFrame(() => { + setShowVideoModal(true); + }); + }; + + const handleGroupClick = (e?: Konva.KonvaEventObject) => { + if (e) { + const target = e.target; + if (target.hasName("playButton") || target.getParent()?.hasName("playButton")) { + return; + } + } + if (groupRef.current) { + onSelect(obj.id, groupRef.current); + } + }; + + if (!image) { + return null; + } return ( <> - { - if (shapeRef.current) { - onSelect(obj.id, shapeRef.current); - } - }} + onClick={handleGroupClick} onDragEnd={(e) => { const node = e.target; onUpdate(obj.id, { @@ -45,38 +126,76 @@ const VideoShape = ({ y: node.y(), }); }} - onTransformEnd={() => { - const node = shapeRef.current; - if (!node) return; - const scaleX = node.scaleX(); - const scaleY = node.scaleY(); - - node.scaleX(1); - node.scaleY(1); - - onUpdate(obj.id, { - x: node.x(), - y: node.y(), - rotation: node.rotation(), - width: Math.max(5, (obj.width || 400) * scaleX), - height: Math.max(5, (obj.height || 300) * scaleY), - }); - }} - /> - {obj.videoUrl && ( - + + + + + + + + {showVideoModal && createPortal( +
setShowVideoModal(false)} + > +
e.stopPropagation()} + > + +
+
, + document.body )} );