base admin
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { type FC } from "react";
|
||||||
|
import { DocumentUpload, Music } from "iconsax-react";
|
||||||
|
|
||||||
|
type AudioUploadZoneProps = {
|
||||||
|
getRootProps: () => React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioUploadZone: FC<AudioUploadZoneProps> = ({ getRootProps, getInputProps }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className="w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<Music
|
||||||
|
size={32}
|
||||||
|
color="#8C90A3"
|
||||||
|
variant="Outline"
|
||||||
|
/>
|
||||||
|
<div className="text-description text-xs">
|
||||||
|
فایل صوتی مورد نظر را آپلود کنید
|
||||||
|
</div>
|
||||||
|
<div className="text-description text-[10px]">
|
||||||
|
mp3, wav, ogg, m4a, aac, flac
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DocumentUpload
|
||||||
|
size={16}
|
||||||
|
color="black"
|
||||||
|
/>
|
||||||
|
<div className="text-xs">آپلود</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioUploadZone;
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Gallery,
|
Gallery,
|
||||||
Link2,
|
Link2,
|
||||||
VideoSquare,
|
VideoSquare,
|
||||||
|
Music,
|
||||||
DocumentText,
|
DocumentText,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Minus,
|
Minus,
|
||||||
@@ -46,6 +47,8 @@ const LayersList = () => {
|
|||||||
return <Link2 size={20} color="black" />
|
return <Link2 size={20} color="black" />
|
||||||
case 'video':
|
case 'video':
|
||||||
return <VideoSquare size={20} color="black" />
|
return <VideoSquare size={20} color="black" />
|
||||||
|
case 'audio':
|
||||||
|
return <Music size={20} color="black" />
|
||||||
case 'document':
|
case 'document':
|
||||||
return <DocumentText size={20} color="black" />
|
return <DocumentText size={20} color="black" />
|
||||||
case 'arrow':
|
case 'arrow':
|
||||||
@@ -93,6 +96,8 @@ const LayersList = () => {
|
|||||||
return 'لینک'
|
return 'لینک'
|
||||||
case 'video':
|
case 'video':
|
||||||
return 'ویدیو'
|
return 'ویدیو'
|
||||||
|
case 'audio':
|
||||||
|
return 'صدا'
|
||||||
case 'document':
|
case 'document':
|
||||||
return 'سند'
|
return 'سند'
|
||||||
case 'arrow':
|
case 'arrow':
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ImageObject,
|
ImageObject,
|
||||||
LinkShape,
|
LinkShape,
|
||||||
VideoShape,
|
VideoShape,
|
||||||
|
AudioShape,
|
||||||
} from "../tools";
|
} from "../tools";
|
||||||
import Table from "@/components/Table";
|
import Table from "@/components/Table";
|
||||||
|
|
||||||
@@ -221,6 +222,10 @@ const ObjectRenderer = ({
|
|||||||
case "video":
|
case "video":
|
||||||
shapeElement = <VideoShape key={obj.id} {...commonProps} />;
|
shapeElement = <VideoShape key={obj.id} {...commonProps} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
shapeElement = <AudioShape key={obj.id} {...commonProps} />;
|
||||||
|
break;
|
||||||
case "document":
|
case "document":
|
||||||
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
SizeSettings,
|
SizeSettings,
|
||||||
LinkSettings,
|
LinkSettings,
|
||||||
VideoSettings,
|
VideoSettings,
|
||||||
|
AudioSettings,
|
||||||
AlignmentSettings,
|
AlignmentSettings,
|
||||||
TransformSettings,
|
TransformSettings,
|
||||||
GridSettings,
|
GridSettings,
|
||||||
@@ -103,7 +104,9 @@ const ObjectSettings = ({
|
|||||||
<VideoSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
|
<VideoSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{roundedSelectedObject.type === "audio" && (
|
||||||
|
<AudioSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
|
||||||
|
)}
|
||||||
|
|
||||||
{(roundedSelectedObject.type === "document" || roundedSelectedObject.type === "sticker") && (
|
{(roundedSelectedObject.type === "document" || roundedSelectedObject.type === "sticker") && (
|
||||||
<SizeSettings
|
<SizeSettings
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type ToolType } from "@/pages/editor/store/editorStore";
|
|||||||
import {
|
import {
|
||||||
ImageGallery,
|
ImageGallery,
|
||||||
VideoInput,
|
VideoInput,
|
||||||
|
AudioInput,
|
||||||
LinkInput,
|
LinkInput,
|
||||||
SelectInstruction,
|
SelectInstruction,
|
||||||
RectangleInstruction,
|
RectangleInstruction,
|
||||||
@@ -25,6 +26,10 @@ const ToolInstructions = ({ tool }: ToolInstructionsProps) => {
|
|||||||
return <VideoInput />;
|
return <VideoInput />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tool === "audio") {
|
||||||
|
return <AudioInput />;
|
||||||
|
}
|
||||||
|
|
||||||
if (tool === "link") {
|
if (tool === "link") {
|
||||||
return <LinkInput />;
|
return <LinkInput />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Sticker,
|
Sticker,
|
||||||
Text,
|
Text,
|
||||||
VideoSquare,
|
VideoSquare,
|
||||||
|
Music,
|
||||||
Minus,
|
Minus,
|
||||||
Link2,
|
Link2,
|
||||||
} from "iconsax-react";
|
} from "iconsax-react";
|
||||||
@@ -21,6 +22,7 @@ const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broke
|
|||||||
{ icon: (color, variant) => <Gallery size={20} color={color} variant={variant} />, tool: "image", label: "گالری" },
|
{ icon: (color, variant) => <Gallery size={20} color={color} variant={variant} />, tool: "image", label: "گالری" },
|
||||||
{ icon: (color, variant) => <Link2 size={20} color={color} variant={variant} />, tool: "link", label: "لینک" },
|
{ icon: (color, variant) => <Link2 size={20} color={color} variant={variant} />, tool: "link", label: "لینک" },
|
||||||
{ icon: (color, variant) => <VideoSquare size={20} color={color} variant={variant} />, tool: "video", label: "ویدیو" },
|
{ icon: (color, variant) => <VideoSquare size={20} color={color} variant={variant} />, tool: "video", label: "ویدیو" },
|
||||||
|
{ icon: (color, variant) => <Music size={20} color={color} variant={variant} />, tool: "audio", label: "صدا" },
|
||||||
{ icon: (color, variant) => <ArrowLeft size={20} color={color} variant={variant} />, tool: "arrow", label: "پیکان" },
|
{ icon: (color, variant) => <ArrowLeft size={20} color={color} variant={variant} />, tool: "arrow", label: "پیکان" },
|
||||||
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
|
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
|
||||||
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
|
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-base font-bold">صدا</div>
|
||||||
|
|
||||||
|
<AudioUploadZone
|
||||||
|
getRootProps={getRootProps}
|
||||||
|
getInputProps={getInputProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{audioObjects.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-bold">فایلهای صوتی آپلود شده</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{audioObjects.map((audioObject) => (
|
||||||
|
<div
|
||||||
|
key={audioObject.id}
|
||||||
|
className="relative flex items-center gap-2 rounded-lg border border-border bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<Music size={20} color="#8C90A3" variant="Bold" />
|
||||||
|
<span className="flex-1 truncate text-xs text-description">
|
||||||
|
فایل صوتی
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteAudio(audioObject.id)}
|
||||||
|
className="absolute -right-1 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color="red" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioInput;
|
||||||
@@ -2,6 +2,7 @@ export { default as ImageGallery } from "./ImageGallery";
|
|||||||
export { default as ImageUpload } from "./ImageUpload";
|
export { default as ImageUpload } from "./ImageUpload";
|
||||||
export { default as DocumentUpload } from "./DocumentUpload";
|
export { default as DocumentUpload } from "./DocumentUpload";
|
||||||
export { default as VideoInput } from "./VideoInput";
|
export { default as VideoInput } from "./VideoInput";
|
||||||
|
export { default as AudioInput } from "./AudioInput";
|
||||||
export { default as LinkInput } from "./LinkInput";
|
export { default as LinkInput } from "./LinkInput";
|
||||||
export { default as SelectInstruction } from "./SelectInstruction";
|
export { default as SelectInstruction } from "./SelectInstruction";
|
||||||
export { default as RectangleInstruction } from "./RectangleInstruction";
|
export { default as RectangleInstruction } from "./RectangleInstruction";
|
||||||
|
|||||||
@@ -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<EditorObject>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioSettings = ({ selectedObject, onUpdate }: AudioSettingsProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
label="آدرس فایل صوتی"
|
||||||
|
value={selectedObject.audioUrl || ""}
|
||||||
|
onChange={(e) => onUpdate(selectedObject.id, { audioUrl: e.target.value })}
|
||||||
|
placeholder="https://example.com/audio.mp3"
|
||||||
|
/>
|
||||||
|
<SizeSettings
|
||||||
|
selectedObject={selectedObject}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
defaultWidth={320}
|
||||||
|
defaultHeight={56}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioSettings;
|
||||||
@@ -4,6 +4,7 @@ export { default as LineSettings } from "./LineSettings";
|
|||||||
export { default as SizeSettings } from "./SizeSettings";
|
export { default as SizeSettings } from "./SizeSettings";
|
||||||
export { default as LinkSettings } from "./LinkSettings";
|
export { default as LinkSettings } from "./LinkSettings";
|
||||||
export { default as VideoSettings } from "./VideoSettings";
|
export { default as VideoSettings } from "./VideoSettings";
|
||||||
|
export { default as AudioSettings } from "./AudioSettings";
|
||||||
export { default as AlignmentSettings } from "./AlignmentSettings";
|
export { default as AlignmentSettings } from "./AlignmentSettings";
|
||||||
export { default as TransformSettings } from "./TransformSettings";
|
export { default as TransformSettings } from "./TransformSettings";
|
||||||
export { default as GridSettings } from "./GridSettings";
|
export { default as GridSettings } from "./GridSettings";
|
||||||
|
|||||||
@@ -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<Konva.Group>(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<MouseEvent>) => {
|
||||||
|
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 (
|
||||||
|
<Group
|
||||||
|
ref={groupRef}
|
||||||
|
id={obj.id}
|
||||||
|
x={obj.x}
|
||||||
|
y={obj.y}
|
||||||
|
rotation={obj.rotation || 0}
|
||||||
|
draggable={draggable}
|
||||||
|
onClick={handleGroupClick}
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
const node = e.target;
|
||||||
|
onUpdate(obj.id, {
|
||||||
|
x: node.x(),
|
||||||
|
y: node.y(),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rect
|
||||||
|
width={containerWidth}
|
||||||
|
height={containerHeight}
|
||||||
|
fill="#f3f4f6"
|
||||||
|
cornerRadius={8}
|
||||||
|
stroke={isSelected ? "#3b82f6" : "#d1d5db"}
|
||||||
|
strokeWidth={isSelected ? 3 : 1}
|
||||||
|
onClick={handleGroupClick}
|
||||||
|
/>
|
||||||
|
<Group name="audioControls" listening={true} onClick={handleGroupClick}>
|
||||||
|
<Circle
|
||||||
|
x={playRadius + 12}
|
||||||
|
y={containerHeight / 2}
|
||||||
|
radius={playRadius}
|
||||||
|
fill="#e5e7eb"
|
||||||
|
listening={true}
|
||||||
|
/>
|
||||||
|
<KonvaText
|
||||||
|
x={playRadius + 12}
|
||||||
|
y={containerHeight / 2}
|
||||||
|
text="▶"
|
||||||
|
fontSize={Math.round(playRadius * 0.9)}
|
||||||
|
fill="#9ca3af"
|
||||||
|
fontFamily="Arial"
|
||||||
|
align="center"
|
||||||
|
verticalAlign="middle"
|
||||||
|
offsetX={playRadius * 0.45}
|
||||||
|
offsetY={playRadius * 0.45}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Rect
|
||||||
|
x={playRadius * 2 + 28}
|
||||||
|
y={barY}
|
||||||
|
width={Math.max(0, containerWidth - playRadius * 2 - 44)}
|
||||||
|
height={barHeight}
|
||||||
|
fill="#d1d5db"
|
||||||
|
cornerRadius={barHeight / 2}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={playRadius * 2 + 28}
|
||||||
|
y={barY}
|
||||||
|
width={Math.max(0, (containerWidth - playRadius * 2 - 44) * 0.35)}
|
||||||
|
height={barHeight}
|
||||||
|
fill="#9ca3af"
|
||||||
|
cornerRadius={barHeight / 2}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<KonvaText
|
||||||
|
x={playRadius * 2 + 28}
|
||||||
|
y={containerHeight * 0.18}
|
||||||
|
text="فایل صوتی (پیشنمایش)"
|
||||||
|
fontSize={11}
|
||||||
|
fill="#6b7280"
|
||||||
|
fontFamily="Arial"
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioShape;
|
||||||
@@ -8,4 +8,5 @@ export { default as ArrowShape } from "./ArrowShape";
|
|||||||
export { default as ImageObject } from "./ImageObject";
|
export { default as ImageObject } from "./ImageObject";
|
||||||
export { default as LinkShape } from "./LinkShape";
|
export { default as LinkShape } from "./LinkShape";
|
||||||
export { default as VideoShape } from "./VideoShape";
|
export { default as VideoShape } from "./VideoShape";
|
||||||
|
export { default as AudioShape } from "./AudioShape";
|
||||||
export type { ShapeProps, TextShapeProps } from "./types";
|
export type { ShapeProps, TextShapeProps } from "./types";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type ToolType =
|
|||||||
| "sticker"
|
| "sticker"
|
||||||
| "document"
|
| "document"
|
||||||
| "video"
|
| "video"
|
||||||
|
| "audio"
|
||||||
| "link"
|
| "link"
|
||||||
| "grid"
|
| "grid"
|
||||||
| "group";
|
| "group";
|
||||||
@@ -68,6 +69,7 @@ export type EditorObject = {
|
|||||||
opacity?: number;
|
opacity?: number;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
videoUrl?: string;
|
videoUrl?: string;
|
||||||
|
audioUrl?: string;
|
||||||
linkUrl?: string;
|
linkUrl?: string;
|
||||||
rotation?: number;
|
rotation?: number;
|
||||||
scaleX?: number;
|
scaleX?: number;
|
||||||
|
|||||||
@@ -226,6 +226,36 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
return (
|
||||||
|
<audio
|
||||||
|
key={obj.id || index}
|
||||||
|
src={obj.audioUrl || ''}
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
style={withEntrance(
|
||||||
|
{
|
||||||
|
...baseStyle,
|
||||||
|
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
||||||
|
height: obj.height ? `${obj.height * scale}px` : `${56 * scale}px`,
|
||||||
|
zIndex: index,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
},
|
||||||
|
obj,
|
||||||
|
index,
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case 'link': {
|
case 'link': {
|
||||||
const isInternalLink = obj.linkUrl?.startsWith('page://');
|
const isInternalLink = obj.linkUrl?.startsWith('page://');
|
||||||
const linkStyle = {
|
const linkStyle = {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type ViewerDataPage = {
|
|||||||
borderRadius?: number;
|
borderRadius?: number;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
videoUrl?: string;
|
videoUrl?: string;
|
||||||
|
audioUrl?: string;
|
||||||
linkUrl?: string;
|
linkUrl?: string;
|
||||||
rotation?: number;
|
rotation?: number;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
@@ -129,6 +130,10 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
baseObject.videoUrl = obj.videoUrl;
|
baseObject.videoUrl = obj.videoUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (obj.type === "audio" && obj.audioUrl) {
|
||||||
|
baseObject.audioUrl = obj.audioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if (obj.type === "link" && obj.linkUrl) {
|
if (obj.type === "link" && obj.linkUrl) {
|
||||||
baseObject.linkUrl = obj.linkUrl;
|
baseObject.linkUrl = obj.linkUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const INITIAL_PAGES_PRELOAD_COUNT = 5;
|
|||||||
|
|
||||||
type MediaAsset = {
|
type MediaAsset = {
|
||||||
url: string;
|
url: string;
|
||||||
kind: 'image' | 'video';
|
kind: 'image' | 'video' | 'audio';
|
||||||
};
|
};
|
||||||
|
|
||||||
const MEDIA_LOAD_TIMEOUT_MS = 15_000;
|
const MEDIA_LOAD_TIMEOUT_MS = 15_000;
|
||||||
@@ -20,6 +20,11 @@ function addVideoUrl(urls: Map<string, MediaAsset>, url?: string) {
|
|||||||
urls.set(url, { url, kind: 'video' });
|
urls.set(url, { url, kind: 'video' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addAudioUrl(urls: Map<string, MediaAsset>, url?: string) {
|
||||||
|
if (!url) return;
|
||||||
|
urls.set(url, { url, kind: 'audio' });
|
||||||
|
}
|
||||||
|
|
||||||
export function extractPageMediaAssets(
|
export function extractPageMediaAssets(
|
||||||
pages: PageData[],
|
pages: PageData[],
|
||||||
documentSettings?: Partial<DocumentSettings>,
|
documentSettings?: Partial<DocumentSettings>,
|
||||||
@@ -38,6 +43,8 @@ export function extractPageMediaAssets(
|
|||||||
addImageUrl(assets, element.imageUrl);
|
addImageUrl(assets, element.imageUrl);
|
||||||
} else if (element.type === 'video') {
|
} else if (element.type === 'video') {
|
||||||
addVideoUrl(assets, element.videoUrl);
|
addVideoUrl(assets, element.videoUrl);
|
||||||
|
} else if (element.type === 'audio') {
|
||||||
|
addAudioUrl(assets, element.audioUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +90,25 @@ function preloadVideo(url: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preloadAudio(url: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const audio = document.createElement('audio');
|
||||||
|
audio.preload = 'auto';
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
audio.onloadeddata = null;
|
||||||
|
audio.onerror = null;
|
||||||
|
audio.removeAttribute('src');
|
||||||
|
audio.load();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.onloadeddata = finish;
|
||||||
|
audio.onerror = finish;
|
||||||
|
audio.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function withTimeout(promise: Promise<void>, ms: number): Promise<void> {
|
function withTimeout(promise: Promise<void>, ms: number): Promise<void> {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
promise,
|
promise,
|
||||||
@@ -94,7 +120,11 @@ function withTimeout(promise: Promise<void>, ms: number): Promise<void> {
|
|||||||
|
|
||||||
export function preloadMediaAsset(asset: MediaAsset): Promise<void> {
|
export function preloadMediaAsset(asset: MediaAsset): Promise<void> {
|
||||||
const load =
|
const load =
|
||||||
asset.kind === 'video' ? preloadVideo(asset.url) : preloadImage(asset.url);
|
asset.kind === 'video'
|
||||||
|
? preloadVideo(asset.url)
|
||||||
|
: asset.kind === 'audio'
|
||||||
|
? preloadAudio(asset.url)
|
||||||
|
: preloadImage(asset.url);
|
||||||
return withTimeout(load, MEDIA_LOAD_TIMEOUT_MS);
|
return withTimeout(load, MEDIA_LOAD_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user