diff --git a/src/components/AudioUploadZone.tsx b/src/components/AudioUploadZone.tsx new file mode 100644 index 0000000..b0c408c --- /dev/null +++ b/src/components/AudioUploadZone.tsx @@ -0,0 +1,38 @@ +import { type FC } from "react"; +import { DocumentUpload, Music } from "iconsax-react"; + +type AudioUploadZoneProps = { + getRootProps: () => React.HTMLAttributes; + getInputProps: () => React.InputHTMLAttributes; +}; + +const AudioUploadZone: FC = ({ getRootProps, getInputProps }) => { + return ( +
+ + +
+ فایل صوتی مورد نظر را آپلود کنید +
+
+ mp3, wav, ogg, m4a, aac, flac +
+
+ +
آپلود
+
+
+ ); +}; + +export default AudioUploadZone; diff --git a/src/pages/editor/components/LayersList.tsx b/src/pages/editor/components/LayersList.tsx index d654dae..7bd349b 100644 --- a/src/pages/editor/components/LayersList.tsx +++ b/src/pages/editor/components/LayersList.tsx @@ -8,6 +8,7 @@ import { Gallery, Link2, VideoSquare, + Music, DocumentText, ArrowLeft, Minus, @@ -46,6 +47,8 @@ const LayersList = () => { return case 'video': return + case 'audio': + return case 'document': return case 'arrow': @@ -93,6 +96,8 @@ const LayersList = () => { return 'لینک' case 'video': return 'ویدیو' + case 'audio': + return 'صدا' case 'document': return 'سند' case 'arrow': diff --git a/src/pages/editor/components/canvas/ObjectRenderer.tsx b/src/pages/editor/components/canvas/ObjectRenderer.tsx index f5b916b..0ad0157 100644 --- a/src/pages/editor/components/canvas/ObjectRenderer.tsx +++ b/src/pages/editor/components/canvas/ObjectRenderer.tsx @@ -13,6 +13,7 @@ import { ImageObject, LinkShape, VideoShape, + AudioShape, } from "../tools"; import Table from "@/components/Table"; @@ -221,6 +222,10 @@ const ObjectRenderer = ({ case "video": shapeElement = ; break; + + case "audio": + shapeElement = ; + break; case "document": shapeElement = ; break; diff --git a/src/pages/editor/components/sidebar/ObjectSettings.tsx b/src/pages/editor/components/sidebar/ObjectSettings.tsx index 5ccebc1..c0d7bdf 100644 --- a/src/pages/editor/components/sidebar/ObjectSettings.tsx +++ b/src/pages/editor/components/sidebar/ObjectSettings.tsx @@ -7,6 +7,7 @@ import { SizeSettings, LinkSettings, VideoSettings, + AudioSettings, AlignmentSettings, TransformSettings, GridSettings, @@ -103,7 +104,9 @@ const ObjectSettings = ({ )} - + {roundedSelectedObject.type === "audio" && ( + + )} {(roundedSelectedObject.type === "document" || roundedSelectedObject.type === "sticker") && ( { return ; } + if (tool === "audio") { + return ; + } + if (tool === "link") { return ; } diff --git a/src/pages/editor/components/sidebar/ToolsBar.tsx b/src/pages/editor/components/sidebar/ToolsBar.tsx index b8538a0..82cd98d 100644 --- a/src/pages/editor/components/sidebar/ToolsBar.tsx +++ b/src/pages/editor/components/sidebar/ToolsBar.tsx @@ -8,6 +8,7 @@ import { Sticker, Text, VideoSquare, + Music, Minus, Link2, } from "iconsax-react"; @@ -21,6 +22,7 @@ const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broke { icon: (color, variant) => , tool: "image", label: "گالری" }, { icon: (color, variant) => , tool: "link", label: "لینک" }, { icon: (color, variant) => , tool: "video", label: "ویدیو" }, + { icon: (color, variant) => , tool: "audio", label: "صدا" }, { icon: (color, variant) => , tool: "arrow", label: "پیکان" }, { icon: (color, variant) => , tool: "line", label: "خط" }, { icon: (color, variant) => , tool: "sticker", label: "استیکر" }, diff --git a/src/pages/editor/components/sidebar/instructions/AudioInput.tsx b/src/pages/editor/components/sidebar/instructions/AudioInput.tsx new file mode 100644 index 0000000..f06c061 --- /dev/null +++ b/src/pages/editor/components/sidebar/instructions/AudioInput.tsx @@ -0,0 +1,96 @@ +import { type FC, useCallback, useMemo } from "react"; +import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore"; +import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData"; +import { CloseCircle, Music } from "iconsax-react"; +import { useDropzone } from "react-dropzone"; +import AudioUploadZone from "@/components/AudioUploadZone"; + +export const AUDIO_ACCEPT = { + "audio/*": [".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac", ".webm"], +} as const; + +const AudioInput: FC = () => { + const { objects, addObject, setSelectedObjectId, setTool, deleteObject } = + useEditorStore(); + const { mutateAsync: uploadFile } = useSingleUpload(); + + const onDrop = useCallback( + async (acceptedFiles: File[]) => { + for (const file of acceptedFiles) { + try { + const result = await uploadFile(file); + const audioUrl = result?.data?.url; + if (!audioUrl) continue; + const audioId = `audio-${Date.now()}-${Math.random()}`; + const newAudio = { + id: audioId, + type: "audio" as ToolType, + x: 100, + y: 100, + width: 320, + height: 56, + audioUrl, + }; + addObject(newAudio); + setSelectedObjectId(newAudio.id); + setTool("select"); + } catch { + // TODO: show error toast + } + } + }, + [addObject, setSelectedObjectId, setTool, uploadFile] + ); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: AUDIO_ACCEPT, + multiple: true, + }); + + const audioObjects = useMemo(() => { + return objects.filter((obj) => obj.type === "audio"); + }, [objects]); + + const handleDeleteAudio = (audioId: string) => { + deleteObject(audioId); + }; + + return ( +
+
صدا
+ + + + {audioObjects.length > 0 && ( +
+
فایل‌های صوتی آپلود شده
+
+ {audioObjects.map((audioObject) => ( +
+ + + فایل صوتی + + +
+ ))} +
+
+ )} +
+ ); +}; + +export default AudioInput; diff --git a/src/pages/editor/components/sidebar/instructions/index.ts b/src/pages/editor/components/sidebar/instructions/index.ts index 6d5ff69..b90fdfb 100644 --- a/src/pages/editor/components/sidebar/instructions/index.ts +++ b/src/pages/editor/components/sidebar/instructions/index.ts @@ -2,6 +2,7 @@ export { default as ImageGallery } from "./ImageGallery"; export { default as ImageUpload } from "./ImageUpload"; export { default as DocumentUpload } from "./DocumentUpload"; export { default as VideoInput } from "./VideoInput"; +export { default as AudioInput } from "./AudioInput"; export { default as LinkInput } from "./LinkInput"; export { default as SelectInstruction } from "./SelectInstruction"; export { default as RectangleInstruction } from "./RectangleInstruction"; diff --git a/src/pages/editor/components/sidebar/settings/AudioSettings.tsx b/src/pages/editor/components/sidebar/settings/AudioSettings.tsx new file mode 100644 index 0000000..85dd8d4 --- /dev/null +++ b/src/pages/editor/components/sidebar/settings/AudioSettings.tsx @@ -0,0 +1,29 @@ +import type { EditorObject } from "../../../store/editorStore"; +import Input from "@/components/Input"; +import SizeSettings from "./SizeSettings"; + +type AudioSettingsProps = { + selectedObject: EditorObject; + onUpdate: (id: string, updates: Partial) => void; +}; + +const AudioSettings = ({ selectedObject, onUpdate }: AudioSettingsProps) => { + return ( + <> + onUpdate(selectedObject.id, { audioUrl: e.target.value })} + placeholder="https://example.com/audio.mp3" + /> + + + ); +}; + +export default AudioSettings; diff --git a/src/pages/editor/components/sidebar/settings/index.ts b/src/pages/editor/components/sidebar/settings/index.ts index 843d7e4..b75ab03 100644 --- a/src/pages/editor/components/sidebar/settings/index.ts +++ b/src/pages/editor/components/sidebar/settings/index.ts @@ -4,6 +4,7 @@ export { default as LineSettings } from "./LineSettings"; export { default as SizeSettings } from "./SizeSettings"; export { default as LinkSettings } from "./LinkSettings"; export { default as VideoSettings } from "./VideoSettings"; +export { default as AudioSettings } from "./AudioSettings"; export { default as AlignmentSettings } from "./AlignmentSettings"; export { default as TransformSettings } from "./TransformSettings"; export { default as GridSettings } from "./GridSettings"; diff --git a/src/pages/editor/components/tools/AudioShape.tsx b/src/pages/editor/components/tools/AudioShape.tsx new file mode 100644 index 0000000..22bdcfd --- /dev/null +++ b/src/pages/editor/components/tools/AudioShape.tsx @@ -0,0 +1,118 @@ +import { useRef } from "react"; +import { Rect, Group, Circle, Text as KonvaText } from "react-konva"; +import Konva from "konva"; +import type { ShapeProps } from "./types"; + +const AudioShape = ({ + obj, + isSelected, + onSelect, + onUpdate, + draggable, +}: ShapeProps) => { + const groupRef = useRef(null); + + const containerWidth = obj.width || 320; + const containerHeight = obj.height || 56; + const barY = containerHeight * 0.55; + const barHeight = Math.max(4, containerHeight * 0.12); + const playRadius = Math.min(18, containerHeight * 0.35); + + const handleGroupClick = (e?: Konva.KonvaEventObject) => { + if (e) { + const target = e.target; + if (target.hasName("audioControls") || target.getParent()?.hasName("audioControls")) { + e.cancelBubble = true; + const evt = e.evt; + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + return; + } + } + if (groupRef.current) { + onSelect(obj.id, groupRef.current, e); + } + }; + + return ( + { + const node = e.target; + onUpdate(obj.id, { + x: node.x(), + y: node.y(), + }); + }} + > + + + + + + + + + + ); +}; + +export default AudioShape; diff --git a/src/pages/editor/components/tools/index.ts b/src/pages/editor/components/tools/index.ts index fb6dc1c..3b2feea 100644 --- a/src/pages/editor/components/tools/index.ts +++ b/src/pages/editor/components/tools/index.ts @@ -8,4 +8,5 @@ export { default as ArrowShape } from "./ArrowShape"; export { default as ImageObject } from "./ImageObject"; export { default as LinkShape } from "./LinkShape"; export { default as VideoShape } from "./VideoShape"; +export { default as AudioShape } from "./AudioShape"; export type { ShapeProps, TextShapeProps } from "./types"; diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts index ff2ae17..886f930 100644 --- a/src/pages/editor/store/editorStore.types.ts +++ b/src/pages/editor/store/editorStore.types.ts @@ -22,6 +22,7 @@ export type ToolType = | "sticker" | "document" | "video" + | "audio" | "link" | "grid" | "group"; @@ -68,6 +69,7 @@ export type EditorObject = { opacity?: number; imageUrl?: string; videoUrl?: string; + audioUrl?: string; linkUrl?: string; rotation?: number; scaleX?: number; diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index cca2738..c845f0d 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -226,6 +226,36 @@ const BookPage = forwardRef( /> ); + case 'audio': + return ( +