Compare commits

..

3 Commits

Author SHA1 Message Date
hamid zarghami 2b4fd31ea2 fix diffrent size in editor and viewer
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-01 11:53:30 +03:30
hamid zarghami 168af3bf95 pages video fix 2026-07-01 11:23:11 +03:30
hamid zarghami 0bef54c51c pages list in editor 2026-07-01 11:20:55 +03:30
5 changed files with 178 additions and 54 deletions
+24 -2
View File
@@ -1,18 +1,38 @@
import { type FC } from "react"; import { type FC } from "react";
import { DocumentUpload, VideoSquare } from "iconsax-react"; import { DocumentUpload, VideoSquare } from "iconsax-react";
import { clx } from "@/helpers/utils";
type VideoUploadZoneProps = { type VideoUploadZoneProps = {
getRootProps: () => React.HTMLAttributes<HTMLDivElement>; getRootProps: () => React.HTMLAttributes<HTMLDivElement>;
getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>; getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>;
isLoading?: boolean;
loadingLabel?: string;
}; };
const VideoUploadZone: FC<VideoUploadZoneProps> = ({ getRootProps, getInputProps }) => { const VideoUploadZone: FC<VideoUploadZoneProps> = ({
getRootProps,
getInputProps,
isLoading = false,
loadingLabel = "در حال آپلود...",
}) => {
return ( return (
<div <div
{...getRootProps()} {...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" className={clx(
"w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl transition-colors",
isLoading
? "pointer-events-none opacity-80"
: "cursor-pointer hover:bg-gray-50"
)}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
{isLoading ? (
<>
<span className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-black" />
<div className="text-description text-xs">{loadingLabel}</div>
</>
) : (
<>
<VideoSquare <VideoSquare
size={32} size={32}
color="#8C90A3" color="#8C90A3"
@@ -28,6 +48,8 @@ const VideoUploadZone: FC<VideoUploadZoneProps> = ({ getRootProps, getInputProps
/> />
<div className="text-xs">آپلود</div> <div className="text-xs">آپلود</div>
</div> </div>
</>
)}
</div> </div>
); );
}; };
@@ -90,7 +90,7 @@ const CatalogPreview: FC<Props> = ({ item, page, size, className, selected }) =>
height: pageHeight, height: pageHeight,
}} }}
> >
<BookPage page={firstPage} scale={1} pageWidth={pageWidth} pageHeight={pageHeight} disableEntranceAnimations showPaperShadow={false} /> <BookPage page={firstPage} scale={1} pageWidth={pageWidth} pageHeight={pageHeight} disableEntranceAnimations staticMedia showPaperShadow={false} />
</div> </div>
</div> </div>
); );
@@ -1,4 +1,4 @@
import { type FC, useCallback, useMemo } from "react"; import { type FC, useCallback, useMemo, useState } from "react";
import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore"; import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore";
import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData"; import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData";
import { CloseCircle } from "iconsax-react"; import { CloseCircle } from "iconsax-react";
@@ -9,9 +9,14 @@ const VideoInput: FC = () => {
const { objects, addObject, setSelectedObjectId, setTool, deleteObject } = const { objects, addObject, setSelectedObjectId, setTool, deleteObject } =
useEditorStore(); useEditorStore();
const { mutateAsync: uploadFile } = useSingleUpload(); const { mutateAsync: uploadFile } = useSingleUpload();
const [isUploading, setIsUploading] = useState(false);
const onDrop = useCallback( const onDrop = useCallback(
async (acceptedFiles: File[]) => { async (acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) return;
setIsUploading(true);
try {
for (const file of acceptedFiles) { for (const file of acceptedFiles) {
try { try {
const result = await uploadFile(file); const result = await uploadFile(file);
@@ -34,6 +39,9 @@ const VideoInput: FC = () => {
// TODO: show error toast // TODO: show error toast
} }
} }
} finally {
setIsUploading(false);
}
}, },
[addObject, setSelectedObjectId, setTool, uploadFile] [addObject, setSelectedObjectId, setTool, uploadFile]
); );
@@ -43,7 +51,8 @@ const VideoInput: FC = () => {
accept: { accept: {
'video/*': ['.mp4', '.webm', '.ogg', '.mov', '.avi'] 'video/*': ['.mp4', '.webm', '.ogg', '.mov', '.avi']
}, },
multiple: true multiple: true,
disabled: isUploading,
}); });
const videoObjects = useMemo(() => { const videoObjects = useMemo(() => {
@@ -61,6 +70,7 @@ const VideoInput: FC = () => {
<VideoUploadZone <VideoUploadZone
getRootProps={getRootProps} getRootProps={getRootProps}
getInputProps={getInputProps} getInputProps={getInputProps}
isLoading={isUploading}
/> />
{videoObjects.length > 0 && ( {videoObjects.length > 0 && (
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Image as KonvaImage, Rect, Group, Circle, Text as KonvaText } from "react-konva"; import { Image as KonvaImage, Rect, Group, Circle, Text as KonvaText } from "react-konva";
import Konva from "konva"; import Konva from "konva";
import useImage from "use-image"; import useImage from "use-image";
@@ -73,6 +73,35 @@ const VideoShape = ({
const containerWidth = obj.width || 400; const containerWidth = obj.width || 400;
const containerHeight = obj.height || 300; const containerHeight = obj.height || 300;
// هم‌تراز با viewer که object-fit: contain دارد
const imageLayout = useMemo(() => {
if (!image) {
return { x: 0, y: 0, width: containerWidth, height: containerHeight };
}
const naturalWidth = image.width || 1;
const naturalHeight = image.height || 1;
const containerRatio = containerWidth / containerHeight;
const imageRatio = naturalWidth / naturalHeight;
let width: number;
let height: number;
if (imageRatio > containerRatio) {
width = containerWidth;
height = containerWidth / imageRatio;
} else {
height = containerHeight;
width = containerHeight * imageRatio;
}
return {
x: (containerWidth - width) / 2,
y: (containerHeight - height) / 2,
width,
height,
};
}, [image, containerWidth, containerHeight]);
const handlePlayClick = (e: Konva.KonvaEventObject<MouseEvent>) => { const handlePlayClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true; e.cancelBubble = true;
const evt = e.evt; const evt = e.evt;
@@ -121,20 +150,30 @@ const VideoShape = ({
<Rect <Rect
width={containerWidth} width={containerWidth}
height={containerHeight} height={containerHeight}
fill="#000000" fill="transparent"
stroke={isSelected ? "#3b82f6" : "#666666"} stroke={isSelected ? "#3b82f6" : "#666666"}
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)} strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
onClick={handleGroupClick} onClick={handleGroupClick}
/> />
{image ? ( {image ? (
<>
<Rect
x={imageLayout.x}
y={imageLayout.y}
width={imageLayout.width}
height={imageLayout.height}
fill="#000000"
onClick={handleGroupClick}
/>
<KonvaImage <KonvaImage
x={0} x={imageLayout.x}
y={0} y={imageLayout.y}
width={containerWidth} width={imageLayout.width}
height={containerHeight} height={imageLayout.height}
image={image} image={image}
onClick={handleGroupClick} onClick={handleGroupClick}
/> />
</>
) : ( ) : (
<Rect <Rect
x={0} x={0}
+57 -4
View File
@@ -38,6 +38,8 @@ const getRasterObjectStyle = (
type RenderObjectOptions = { type RenderObjectOptions = {
skipEntrance?: boolean; skipEntrance?: boolean;
/** در پیش‌نمایش (لیست صفحات و کاتالوگ) رسانه تعاملی نباشد */
staticMedia?: boolean;
}; };
type BookPageProps = { type BookPageProps = {
@@ -56,8 +58,10 @@ type BookPageProps = {
backgroundImageUrl?: string; backgroundImageUrl?: string;
/** فاز انیمیشن ورود — از BookViewer کنترل می‌شود */ /** فاز انیمیشن ورود — از BookViewer کنترل می‌شود */
entrancePhase?: EntrancePhase; entrancePhase?: EntrancePhase;
/** در پیش‌نمایش کاتالوگ انیمیشن ورود غیرفعال باشد */ /** در پیش‌نمایش کاتالوگ انیمی ورود غیرفعال باشد */
disableEntranceAnimations?: boolean; disableEntranceAnimations?: boolean;
/** ویدیو/صوت فقط به‌صورت کاور — بدون کنترل و بدون کلیک */
staticMedia?: boolean;
/** سایهٔ لبهٔ کاغذ (گرادیان داخل DOM — سازگار با iOS قدیمی) */ /** سایهٔ لبهٔ کاغذ (گرادیان داخل DOM — سازگار با iOS قدیمی) */
showPaperShadow?: boolean; showPaperShadow?: boolean;
/** ورق سخت به‌جای clip-path برای iOS قدیمی */ /** ورق سخت به‌جای clip-path برای iOS قدیمی */
@@ -78,7 +82,7 @@ type BookPageProps = {
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
*/ */
const BookPage = forwardRef<HTMLDivElement, BookPageProps>( const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => { ({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase; const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase;
// تابع برای تبدیل opacity به رنگ (مثل editor) // تابع برای تبدیل opacity به رنگ (مثل editor)
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد // در editor، getColorWithOpacity انتظار opacity 0-100 دارد
@@ -243,6 +247,35 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
); );
case 'video': case 'video':
if (options.staticMedia) {
return (
<video
key={obj.id || index}
src={obj.videoUrl || ''}
muted
playsInline
preload="metadata"
style={applyStyle(
{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain',
zIndex: index,
pointerEvents: 'none',
},
obj,
index,
)}
onLoadedMetadata={(e) => {
const video = e.currentTarget;
if (video.currentTime === 0) {
video.currentTime = 0.1;
}
}}
/>
);
}
return ( return (
<video <video
key={obj.id || index} key={obj.id || index}
@@ -273,6 +306,26 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
); );
case 'audio': case 'audio':
if (options.staticMedia) {
return (
<div
key={obj.id || index}
style={applyStyle(
{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
height: obj.height ? `${obj.height * scale}px` : `${56 * scale}px`,
backgroundColor: '#f3f4f6',
borderRadius: `${4 * scale}px`,
zIndex: index,
pointerEvents: 'none',
},
obj,
index,
)}
/>
);
}
return ( return (
<audio <audio
key={obj.id || index} key={obj.id || index}
@@ -837,7 +890,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
: null; : null;
if (!maskShape) { if (!maskShape) {
return renderObjectContent(obj, index); return renderObjectContent(obj, index, { staticMedia });
} }
const layout = getMaskedLayout(obj, maskShape, scale); const layout = getMaskedLayout(obj, maskShape, scale);
@@ -847,7 +900,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
const content = renderObjectContent( const content = renderObjectContent(
{ ...obj, x: (obj.x || 0) - unionLeft, y: (obj.y || 0) - unionTop }, { ...obj, x: (obj.x || 0) - unionLeft, y: (obj.y || 0) - unionTop },
index, index,
{ skipEntrance: true }, { skipEntrance: true, staticMedia },
); );
if (!content) return null; if (!content) return null;