Compare commits
2 Commits
5e314bb581
...
1ed198ca69
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed198ca69 | |||
| d94bf77522 |
@@ -32,6 +32,7 @@ function getPageDataFromEditorPage(page: Page): PageData | null {
|
||||
backgroundColor: page.backgroundColor,
|
||||
backgroundGradient: page.backgroundGradient,
|
||||
backgroundImageUrl: page.backgroundImageUrl,
|
||||
backgroundVideoUrl: page.backgroundVideoUrl,
|
||||
objects: page.objects,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -76,6 +76,9 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
||||
? currentPage.backgroundColor
|
||||
: '#ffffff';
|
||||
const bgGradient = currentPage?.backgroundGradient;
|
||||
const hasVideoBackground =
|
||||
currentPage?.backgroundType === "video" &&
|
||||
Boolean(currentPage.backgroundVideoUrl);
|
||||
|
||||
const [bgImage] = useImage(
|
||||
currentPage?.backgroundType === 'image'
|
||||
@@ -527,10 +530,20 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
||||
<div className="absolute -top-6 -left-6 w-6 h-6 bg-slate-200 border border-slate-300" />
|
||||
<div
|
||||
ref={stageWrapRef}
|
||||
className="shadow-lg"
|
||||
className="relative shadow-lg"
|
||||
onDragOver={handleSidebarDragOver}
|
||||
onDrop={handleSidebarDrop}
|
||||
>
|
||||
{hasVideoBackground && (
|
||||
<video
|
||||
src={currentPage?.backgroundVideoUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
<Stage
|
||||
ref={stageRef}
|
||||
width={stageSize.width * finalScale}
|
||||
@@ -549,16 +562,18 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Layer listening={false}>
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
fill={bgColor}
|
||||
{...backgroundGradientProps}
|
||||
listening={false}
|
||||
/>
|
||||
{currentPage?.backgroundType === 'image' && bgImage && (
|
||||
{!hasVideoBackground && (
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
fill={bgColor}
|
||||
{...backgroundGradientProps}
|
||||
listening={false}
|
||||
/>
|
||||
)}
|
||||
{!hasVideoBackground && currentPage?.backgroundType === 'image' && bgImage && (
|
||||
<KonvaImage
|
||||
image={bgImage}
|
||||
x={0}
|
||||
|
||||
@@ -10,6 +10,9 @@ function isPageEmptyForPreview(page: Page): boolean {
|
||||
if (page.backgroundType === 'image' && page.backgroundImageUrl.trim() !== '') {
|
||||
return false
|
||||
}
|
||||
if (page.backgroundType === 'video' && page.backgroundVideoUrl.trim() !== '') {
|
||||
return false
|
||||
}
|
||||
if (page.backgroundType === 'gradient') return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const BACKGROUND_TYPE_OPTIONS: { value: BackgroundType; label: string }[] = [
|
||||
{ value: 'color', label: 'رنگ' },
|
||||
{ value: 'gradient', label: 'گرادیانت' },
|
||||
{ value: 'image', label: 'تصویر' },
|
||||
{ value: 'video', label: 'ویدیو' },
|
||||
]
|
||||
|
||||
const SettingsPanel = () => {
|
||||
@@ -38,7 +39,7 @@ const SettingsPanel = () => {
|
||||
updateCurrentPageBackground,
|
||||
} = useEditorStore()
|
||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
||||
const { mutateAsync: uploadFile, isPending: imageLoading } = useSingleUpload()
|
||||
const { mutateAsync: uploadFile, isPending: mediaLoading } = useSingleUpload()
|
||||
const currentPage = pages.find((page) => page.id === currentPageId)
|
||||
const backgroundType = currentPage?.backgroundType ?? 'color'
|
||||
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
|
||||
@@ -48,6 +49,7 @@ const SettingsPanel = () => {
|
||||
angle: 135,
|
||||
}
|
||||
const backgroundImageUrl = currentPage?.backgroundImageUrl ?? ''
|
||||
const backgroundVideoUrl = currentPage?.backgroundVideoUrl ?? ''
|
||||
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
|
||||
backgroundImageUrl ? [backgroundImageUrl] : []
|
||||
)
|
||||
@@ -66,6 +68,11 @@ const SettingsPanel = () => {
|
||||
)
|
||||
}, [backgroundImageUrl])
|
||||
|
||||
useEffect(() => {
|
||||
if (backgroundType !== 'video') return
|
||||
setUploadedPreviews(backgroundVideoUrl ? [backgroundVideoUrl] : [])
|
||||
}, [backgroundType, backgroundVideoUrl])
|
||||
|
||||
const handleImageFiles = async (files: File[]) => {
|
||||
if (files.length === 0) return
|
||||
const file = files[files.length - 1]
|
||||
@@ -83,10 +90,31 @@ const SettingsPanel = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleVideoFiles = async (files: File[]) => {
|
||||
if (files.length === 0) return
|
||||
const file = files[files.length - 1]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const result = await uploadFile({ file })
|
||||
const videoUrl = result?.data?.url
|
||||
if (!videoUrl) return
|
||||
|
||||
updateCurrentPageBackground({ backgroundVideoUrl: videoUrl })
|
||||
setUploadedPreviews([videoUrl])
|
||||
} catch {
|
||||
// TODO: show error toast
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviewChange = (previews: string[]) => {
|
||||
setUploadedPreviews(previews)
|
||||
if (previews.length === 0) {
|
||||
updateCurrentPageBackground({ backgroundImageUrl: '' })
|
||||
if (backgroundType === 'image') {
|
||||
updateCurrentPageBackground({ backgroundImageUrl: '' })
|
||||
} else if (backgroundType === 'video') {
|
||||
updateCurrentPageBackground({ backgroundVideoUrl: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +177,7 @@ const SettingsPanel = () => {
|
||||
<h3 className="text-sm mb-4">پس زمینه</h3>
|
||||
|
||||
{/* نوع پسزمینه */}
|
||||
<div className="flex items-center gap-5 mb-4">
|
||||
<div className="flex flex-wrap items-center gap-5 mb-4">
|
||||
{BACKGROUND_TYPE_OPTIONS.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-1.5 cursor-pointer select-none">
|
||||
<span className="text-xs text-gray-600">{opt.label}</span>
|
||||
@@ -166,6 +194,8 @@ const SettingsPanel = () => {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(opt.value !== 'image' ? { backgroundImageUrl: '' } : {}),
|
||||
...(opt.value !== 'video' ? { backgroundVideoUrl: '' } : {}),
|
||||
})
|
||||
}
|
||||
className={clx(
|
||||
@@ -301,7 +331,7 @@ const SettingsPanel = () => {
|
||||
{/* ─── آپلود تصویر ─── */}
|
||||
{backgroundType === 'image' && (
|
||||
<div>
|
||||
{imageLoading ? (
|
||||
{mediaLoading ? (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-8">
|
||||
<span className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-800" />
|
||||
<span className="text-xs text-gray-500">در حال آپلود...</span>
|
||||
@@ -317,6 +347,44 @@ const SettingsPanel = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{backgroundType === 'video' && (
|
||||
<div>
|
||||
{mediaLoading ? (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-8">
|
||||
<span className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-800" />
|
||||
<span className="text-xs text-gray-500">در حال آپلود...</span>
|
||||
</div>
|
||||
) : (
|
||||
<UploadBoxDraggble
|
||||
label="ویدیوی پسزمینه را آپلود کنید"
|
||||
onChange={handleVideoFiles}
|
||||
isMultiple={false}
|
||||
hidePreview
|
||||
/>
|
||||
)}
|
||||
{backgroundVideoUrl && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<video
|
||||
src={backgroundVideoUrl}
|
||||
controls
|
||||
muted
|
||||
className="w-full h-36 rounded-lg border border-border bg-black object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateCurrentPageBackground({ backgroundVideoUrl: '' })
|
||||
setUploadedPreviews([])
|
||||
}}
|
||||
className="text-xs text-red-500 hover:text-red-600"
|
||||
>
|
||||
حذف ویدیو
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import Konva from "konva";
|
||||
import { useEditorStore, type EditorObject } from "@/pages/editor/store/editorStore";
|
||||
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
|
||||
import { createDrawingObject } from "./drawingUtils";
|
||||
import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
|
||||
|
||||
export const useDrawingHandlers = () => {
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
@@ -82,6 +83,7 @@ export const useDrawingHandlers = () => {
|
||||
letterSpacing: defaults.letterSpacing,
|
||||
wordSpacing: defaults.wordSpacing,
|
||||
height: defaults.fontSize || 24,
|
||||
textMaxWidth: DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||
};
|
||||
useEditorStore.getState().commitObjectHistoryBeforeChange();
|
||||
addObject(newText);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { EditorObject } from "../../../store/editorStore";
|
||||
import Input from "@/components/Input";
|
||||
import ColorPicker from "@/components/ColorPicker";
|
||||
import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
|
||||
|
||||
type TextSettingsProps = {
|
||||
selectedObject: EditorObject;
|
||||
@@ -60,6 +61,18 @@ const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label="حداکثر عرض متن"
|
||||
type="number"
|
||||
value={selectedObject.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX}
|
||||
step="1"
|
||||
min="80"
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
textMaxWidth: Math.max(80, parseNumber(e.target.value) || DEFAULT_TEXT_MAX_WIDTH_PX),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">چینش متن</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
|
||||
@@ -4,8 +4,10 @@ import Konva from "konva";
|
||||
import type { TextShapeProps } from "./types";
|
||||
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
|
||||
import {
|
||||
DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||
getEffectiveTextWidth,
|
||||
measureTextBlock,
|
||||
resolveTextMaxWidth,
|
||||
usesWrappedLayout,
|
||||
} from "@/pages/editor/utils/textStyle";
|
||||
|
||||
@@ -58,6 +60,7 @@ const TextShape = ({
|
||||
const shapeRef = useRef<Konva.Text>(null);
|
||||
const groupRef = useRef<Konva.Group>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [maxTextWidth, setMaxTextWidth] = useState<number>(DEFAULT_TEXT_MAX_WIDTH_PX);
|
||||
const fontSize = obj.fontSize || 24;
|
||||
const allowWrap = usesWrappedLayout(
|
||||
obj.text,
|
||||
@@ -86,6 +89,7 @@ const TextShape = ({
|
||||
const groupNode = groupRef.current;
|
||||
if (!textNode || !groupNode) return;
|
||||
if (groupNode.scaleX() !== 1 || groupNode.scaleY() !== 1) return;
|
||||
const stage = textNode.getStage();
|
||||
|
||||
textNode._setTextData();
|
||||
|
||||
@@ -110,6 +114,14 @@ const TextShape = ({
|
||||
lineHeight: obj.lineHeight ?? 1.2,
|
||||
});
|
||||
const konvaWidth = Math.max(1, Math.ceil(rw));
|
||||
const resolvedMaxWidth = resolveTextMaxWidth({
|
||||
anchorX: obj.x || 0,
|
||||
pageWidth: stage?.width(),
|
||||
defaultMaxWidth: obj.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||
});
|
||||
if (Math.abs(resolvedMaxWidth - maxTextWidth) > 1) {
|
||||
setMaxTextWidth(resolvedMaxWidth);
|
||||
}
|
||||
const nextWidth = getEffectiveTextWidth(
|
||||
konvaWidth,
|
||||
obj.text,
|
||||
@@ -122,16 +134,33 @@ const TextShape = ({
|
||||
},
|
||||
!allowWrap,
|
||||
) ?? konvaWidth;
|
||||
const clampedWidth = Math.min(nextWidth, resolvedMaxWidth);
|
||||
const forcedWrapByWidth = clampedWidth < nextWidth - 1;
|
||||
let measuredHeight = rh;
|
||||
if (forcedWrapByWidth) {
|
||||
const wrappedMeasureNode = textNode.clone({
|
||||
width: clampedWidth,
|
||||
wrap: "word",
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
wrappedMeasureNode._setTextData();
|
||||
const { height: wrappedHeight } = wrappedMeasureNode.getClientRect({
|
||||
skipTransform: true,
|
||||
});
|
||||
wrappedMeasureNode.destroy();
|
||||
measuredHeight = Math.max(measuredHeight, wrappedHeight);
|
||||
}
|
||||
const nextHeight = Math.max(
|
||||
1,
|
||||
Math.ceil(rh),
|
||||
Math.ceil(measuredHeight),
|
||||
cssMeasured.height,
|
||||
);
|
||||
const widthChanged = Math.abs((obj.width || 0) - nextWidth) > 1;
|
||||
const widthChanged = Math.abs((obj.width || 0) - clampedWidth) > 1;
|
||||
const heightChanged = Math.abs((obj.height || 0) - nextHeight) > 1;
|
||||
|
||||
if (widthChanged || heightChanged) {
|
||||
onUpdate(obj.id, { width: nextWidth, height: nextHeight });
|
||||
onUpdate(obj.id, { width: clampedWidth, height: nextHeight });
|
||||
}
|
||||
textNode.getLayer()?.batchDraw();
|
||||
};
|
||||
@@ -191,7 +220,10 @@ const TextShape = ({
|
||||
obj.lineHeight,
|
||||
obj.width,
|
||||
obj.height,
|
||||
obj.x,
|
||||
obj.textMaxWidth,
|
||||
allowWrap,
|
||||
maxTextWidth,
|
||||
onUpdate,
|
||||
]);
|
||||
|
||||
@@ -199,6 +231,8 @@ const TextShape = ({
|
||||
const konvaFontStyle = useMemo(() => getKonvaFontStyle(obj.fontWeight), [obj.fontWeight]);
|
||||
const fillColor = useMemo(() => getColorWithOpacity(obj.fill, obj.opacity), [obj.fill, obj.opacity]);
|
||||
const textAlign = obj.textAlign ?? "right";
|
||||
const wrapByMaxWidth = (obj.width ?? 0) >= maxTextWidth - 1;
|
||||
const shouldWrap = allowWrap || wrapByMaxWidth;
|
||||
|
||||
const handleDblClick = () => {
|
||||
if (!shapeRef.current || !groupRef.current) return;
|
||||
@@ -238,7 +272,8 @@ const TextShape = ({
|
||||
position: "absolute",
|
||||
top: `${areaPosition.y}px`,
|
||||
left: `${areaPosition.x}px`,
|
||||
width: `${box.width}px`,
|
||||
width: `${Math.min(box.width, maxTextWidth * stageScale)}px`,
|
||||
maxWidth: `${maxTextWidth * stageScale}px`,
|
||||
minHeight: `${box.height}px`,
|
||||
fontSize: `${(obj.fontSize || 24) * stageScale}px`,
|
||||
fontFamily: fontFamily,
|
||||
@@ -358,9 +393,9 @@ const TextShape = ({
|
||||
fontStyle={konvaFontStyle}
|
||||
letterSpacing={obj.letterSpacing ?? 0}
|
||||
lineHeight={obj.lineHeight ?? 1.2}
|
||||
width={allowWrap ? obj.width || undefined : undefined}
|
||||
width={shouldWrap ? obj.width || maxTextWidth : undefined}
|
||||
align={textAlign}
|
||||
wrap={allowWrap ? "word" : "none"}
|
||||
wrap={shouldWrap ? "word" : "none"}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -65,6 +65,7 @@ export const createInitialPage = (name: string): Page => ({
|
||||
angle: 135,
|
||||
},
|
||||
backgroundImageUrl: "",
|
||||
backgroundVideoUrl: "",
|
||||
});
|
||||
|
||||
export const getDefaultPageName = (index: number): string =>
|
||||
|
||||
@@ -70,6 +70,7 @@ type PageBackgroundSettings = Pick<
|
||||
| "backgroundColor"
|
||||
| "backgroundGradient"
|
||||
| "backgroundImageUrl"
|
||||
| "backgroundVideoUrl"
|
||||
>;
|
||||
|
||||
type EditorStoreType = {
|
||||
@@ -191,6 +192,7 @@ const DEFAULT_DOCUMENT_SETTINGS: DocumentSettings = {
|
||||
angle: 135,
|
||||
},
|
||||
backgroundImageUrl: "",
|
||||
backgroundVideoUrl: "",
|
||||
};
|
||||
|
||||
export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
@@ -858,6 +860,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
backgroundColor: pageToDuplicate.backgroundColor,
|
||||
backgroundGradient: pageToDuplicate.backgroundGradient,
|
||||
backgroundImageUrl: pageToDuplicate.backgroundImageUrl,
|
||||
backgroundVideoUrl: pageToDuplicate.backgroundVideoUrl,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
@@ -1058,6 +1061,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
},
|
||||
backgroundImageUrl:
|
||||
page.backgroundImageUrl ?? fallbackSettings.backgroundImageUrl ?? "",
|
||||
backgroundVideoUrl:
|
||||
page.backgroundVideoUrl ?? fallbackSettings.backgroundVideoUrl ?? "",
|
||||
}));
|
||||
const firstPage = normalizedPages[0];
|
||||
const currentSettings = get().documentSettings;
|
||||
|
||||
@@ -61,6 +61,8 @@ export type EditorObject = {
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
text?: string;
|
||||
/** حداکثر عرض باکس متن (px) برای wrap پیشفرض متنهای بلند */
|
||||
textMaxWidth?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
@@ -119,10 +121,11 @@ export type Page = {
|
||||
backgroundColor: string;
|
||||
backgroundGradient?: LinearGradient;
|
||||
backgroundImageUrl: string;
|
||||
backgroundVideoUrl: string;
|
||||
};
|
||||
|
||||
export type DisplayStyle = "single" | "double";
|
||||
export type BackgroundType = "color" | "gradient" | "image";
|
||||
export type BackgroundType = "color" | "gradient" | "image" | "video";
|
||||
|
||||
export type DocumentSettings = {
|
||||
displayStyle: DisplayStyle;
|
||||
@@ -134,6 +137,7 @@ export type DocumentSettings = {
|
||||
backgroundColor: string;
|
||||
backgroundGradient?: LinearGradient;
|
||||
backgroundImageUrl: string;
|
||||
backgroundVideoUrl: string;
|
||||
};
|
||||
|
||||
/** تصویر آپلودشده در گالری کاتالوگ (مشترک بین همهٔ صفحات) */
|
||||
|
||||
@@ -63,6 +63,39 @@ export const usesWrappedLayout = (
|
||||
|
||||
/** Extra px so HTML layout does not wrap tighter than Konva/canvas metrics. */
|
||||
export const SINGLE_LINE_WIDTH_SLACK_PX = 12;
|
||||
export const DEFAULT_TEXT_MAX_WIDTH_PX = 520;
|
||||
const MIN_TEXT_MAX_WIDTH_PX = 80;
|
||||
|
||||
/**
|
||||
* Maximum width for text boxes.
|
||||
* - never larger than page/view bounds
|
||||
* - has a sane default for long single-line text
|
||||
*/
|
||||
export const resolveTextMaxWidth = (args: {
|
||||
anchorX: number;
|
||||
pageWidth?: number;
|
||||
defaultMaxWidth?: number;
|
||||
edgePadding?: number;
|
||||
}): number => {
|
||||
const {
|
||||
anchorX,
|
||||
pageWidth,
|
||||
defaultMaxWidth = DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||
edgePadding = 8,
|
||||
} = args;
|
||||
|
||||
const fromAnchor = Math.floor(anchorX - edgePadding);
|
||||
const fromPage =
|
||||
pageWidth !== undefined
|
||||
? Math.floor(Math.min(anchorX - edgePadding, pageWidth - edgePadding))
|
||||
: fromAnchor;
|
||||
|
||||
const hardLimit = Math.max(MIN_TEXT_MAX_WIDTH_PX, fromPage);
|
||||
return Math.max(
|
||||
MIN_TEXT_MAX_WIDTH_PX,
|
||||
Math.min(defaultMaxWidth, hardLimit),
|
||||
);
|
||||
};
|
||||
|
||||
export type TextMeasureOptions = {
|
||||
fontSize: number;
|
||||
|
||||
@@ -5,8 +5,10 @@ import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/uti
|
||||
import { getCustomShapePointBounds } from '@/pages/editor/utils/customShape';
|
||||
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
||||
import {
|
||||
DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||
getCssFontWeight,
|
||||
getViewerTextLayout,
|
||||
resolveTextMaxWidth,
|
||||
usesWrappedLayout,
|
||||
} from '@/pages/editor/utils/textStyle';
|
||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||
@@ -48,7 +50,7 @@ type BookPageProps = {
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
onLinkClick?: (linkUrl: string) => void;
|
||||
backgroundType?: 'color' | 'gradient' | 'image';
|
||||
backgroundType?: 'color' | 'gradient' | 'image' | 'video';
|
||||
backgroundColor?: string;
|
||||
backgroundGradient?: {
|
||||
from: string;
|
||||
@@ -56,6 +58,7 @@ type BookPageProps = {
|
||||
angle: number;
|
||||
};
|
||||
backgroundImageUrl?: string;
|
||||
backgroundVideoUrl?: string;
|
||||
/** فاز انیمیشن ورود — از BookViewer کنترل میشود */
|
||||
entrancePhase?: EntrancePhase;
|
||||
/** در پیشنمایش کاتالوگ انیمی션 ورود غیرفعال باشد */
|
||||
@@ -82,7 +85,7 @@ type BookPageProps = {
|
||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||
*/
|
||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
||||
const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase;
|
||||
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
||||
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
||||
@@ -198,14 +201,39 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
measureOpts,
|
||||
wrapped,
|
||||
});
|
||||
const maxWidthPx = Math.ceil(
|
||||
resolveTextMaxWidth({
|
||||
anchorX: (obj.x || 0) * scale,
|
||||
pageWidth: pageWidth ?? 794 * scale,
|
||||
defaultMaxWidth: (obj.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX) * scale,
|
||||
}),
|
||||
);
|
||||
const wrappedWidthPx =
|
||||
obj.width !== undefined ? Math.ceil(obj.width * scale) : undefined;
|
||||
const wrapByMaxWidth =
|
||||
(wrapped ? wrappedWidthPx : textLayout.widthPx) !== undefined &&
|
||||
(wrapped ? wrappedWidthPx : textLayout.widthPx)! > maxWidthPx;
|
||||
const finalWrapped = wrapped || wrapByMaxWidth;
|
||||
const resolvedWidthPx = finalWrapped
|
||||
? (wrappedWidthPx !== undefined
|
||||
? Math.min(wrappedWidthPx, maxWidthPx)
|
||||
: (textLayout.widthPx !== undefined
|
||||
? Math.min(textLayout.widthPx, maxWidthPx)
|
||||
: undefined))
|
||||
: textLayout.widthPx;
|
||||
const anchorRightPx = Math.round((obj.x || 0) * scale);
|
||||
const resolvedLeftPx =
|
||||
resolvedWidthPx !== undefined
|
||||
? anchorRightPx - resolvedWidthPx
|
||||
: textLayout.leftPx;
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={applyStyle(
|
||||
{
|
||||
...textBaseStyle,
|
||||
left: `${textLayout.leftPx}px`,
|
||||
width: textLayout.widthPx !== undefined ? `${textLayout.widthPx}px` : 'max-content',
|
||||
left: `${resolvedLeftPx}px`,
|
||||
width: resolvedWidthPx !== undefined ? `${resolvedWidthPx}px` : 'max-content',
|
||||
fontSize: `${fontSize * scale}px`,
|
||||
fontFamily: getFontFamily(obj.fontFamily),
|
||||
fontWeight: getCssFontWeight(obj.fontWeight),
|
||||
@@ -213,10 +241,11 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
textAlign: textLayout.textAlign,
|
||||
direction: 'rtl',
|
||||
color: getColorWithOpacity(obj.fill, obj.opacity),
|
||||
whiteSpace: wrapped ? 'pre-wrap' : 'nowrap',
|
||||
overflowWrap: wrapped ? 'break-word' : 'normal',
|
||||
whiteSpace: finalWrapped ? 'pre-wrap' : 'nowrap',
|
||||
overflowWrap: finalWrapped ? 'break-word' : 'normal',
|
||||
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
||||
boxSizing: 'content-box',
|
||||
zIndex: index,
|
||||
transform: textLayout.transform,
|
||||
transformOrigin: textLayout.transformOrigin,
|
||||
},
|
||||
@@ -933,6 +962,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff';
|
||||
const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient;
|
||||
const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? '';
|
||||
const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? '';
|
||||
|
||||
const bgStyle: React.CSSProperties =
|
||||
effectiveBackgroundType === 'image' && effectiveBackgroundImageUrl
|
||||
@@ -968,16 +998,38 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
}}
|
||||
>
|
||||
{!useHardPage && (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
...bgStyle,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
...bgStyle,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
{effectiveBackgroundType === 'video' && effectiveBackgroundVideoUrl && (
|
||||
<video
|
||||
aria-hidden
|
||||
src={effectiveBackgroundVideoUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||
{page.elements.map((element, index) => renderObject(element, index, page.elements))}
|
||||
|
||||
@@ -185,9 +185,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
||||
const backgroundGradient = documentSettings?.backgroundGradient;
|
||||
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? "";
|
||||
const backgroundVideoUrl = documentSettings?.backgroundVideoUrl ?? "";
|
||||
const pageBackgroundDefaults = useMemo(
|
||||
() => ({ backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl }),
|
||||
[backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl],
|
||||
() => ({ backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl }),
|
||||
[backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -610,6 +611,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
backgroundColor={background.backgroundColor}
|
||||
backgroundGradient={background.backgroundGradient}
|
||||
backgroundImageUrl={background.backgroundImageUrl}
|
||||
backgroundVideoUrl={background.backgroundVideoUrl}
|
||||
useHardPage={legacyIOS}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -94,6 +94,7 @@ const Magnifier = memo(
|
||||
backgroundColor={background.backgroundColor}
|
||||
backgroundGradient={background.backgroundGradient}
|
||||
backgroundImageUrl={background.backgroundImageUrl}
|
||||
backgroundVideoUrl={background.backgroundVideoUrl}
|
||||
useHardPage={useHardPage}
|
||||
disableEntranceAnimations
|
||||
showPaperShadow={false}
|
||||
|
||||
@@ -5,7 +5,7 @@ export type PageData = {
|
||||
width: number;
|
||||
height: number;
|
||||
elements: EditorObject[];
|
||||
backgroundType?: "color" | "gradient" | "image";
|
||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||
backgroundColor?: string;
|
||||
backgroundGradient?: {
|
||||
from: string;
|
||||
@@ -13,5 +13,6 @@ export type PageData = {
|
||||
angle: number;
|
||||
};
|
||||
backgroundImageUrl?: string;
|
||||
backgroundVideoUrl?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { PageData } from "../types";
|
||||
type ViewerDataPage = {
|
||||
id: string;
|
||||
name: string;
|
||||
backgroundType?: "color" | "gradient" | "image";
|
||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||
backgroundColor?: string;
|
||||
backgroundGradient?: {
|
||||
from: string;
|
||||
@@ -13,6 +13,7 @@ type ViewerDataPage = {
|
||||
angle: number;
|
||||
};
|
||||
backgroundImageUrl?: string;
|
||||
backgroundVideoUrl?: string;
|
||||
objects: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -200,6 +201,7 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
||||
backgroundColor: page.backgroundColor,
|
||||
backgroundGradient: page.backgroundGradient,
|
||||
backgroundImageUrl: page.backgroundImageUrl,
|
||||
backgroundVideoUrl: page.backgroundVideoUrl,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { PageData } from "@/pages/viewer/types";
|
||||
|
||||
export type PageBackgroundDefaults = {
|
||||
backgroundType?: "color" | "gradient" | "image";
|
||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||
backgroundColor?: string;
|
||||
backgroundGradient?: { from: string; to: string; angle: number };
|
||||
backgroundImageUrl?: string;
|
||||
backgroundVideoUrl?: string;
|
||||
};
|
||||
|
||||
/** ترکیب پسزمینهٔ اختصاصی صفحه با مقادیر پیشفرض سند (همان قاعدهٔ استفادهشده در BookViewer) */
|
||||
@@ -14,5 +15,6 @@ export function resolvePageBackground(page: PageData, defaults: PageBackgroundDe
|
||||
backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff",
|
||||
backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 },
|
||||
backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "",
|
||||
backgroundVideoUrl: page.backgroundVideoUrl ?? defaults.backgroundVideoUrl ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export function extractPageMediaAssets(
|
||||
for (const page of pages) {
|
||||
if (page.backgroundType === 'image') {
|
||||
addImageUrl(assets, page.backgroundImageUrl);
|
||||
} else if (page.backgroundType === 'video') {
|
||||
addVideoUrl(assets, page.backgroundVideoUrl);
|
||||
}
|
||||
|
||||
for (const element of page.elements) {
|
||||
@@ -51,6 +53,8 @@ export function extractPageMediaAssets(
|
||||
|
||||
if (documentSettings?.backgroundType === 'image') {
|
||||
addImageUrl(assets, documentSettings.backgroundImageUrl);
|
||||
} else if (documentSettings?.backgroundType === 'video') {
|
||||
addVideoUrl(assets, documentSettings.backgroundVideoUrl);
|
||||
}
|
||||
|
||||
return Array.from(assets.values());
|
||||
|
||||
Reference in New Issue
Block a user