background video

This commit is contained in:
hamid zarghami
2026-07-02 12:08:29 +03:30
parent 5e314bb581
commit d94bf77522
14 changed files with 164 additions and 33 deletions
@@ -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,
},
],
+17 -2
View File
@@ -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,6 +562,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
onDragEnd={handleDragEnd}
>
<Layer listening={false}>
{!hasVideoBackground && (
<Rect
x={0}
y={0}
@@ -558,7 +572,8 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
{...backgroundGradientProps}
listening={false}
/>
{currentPage?.backgroundType === 'image' && bgImage && (
)}
{!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
}
+71 -3
View File
@@ -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) {
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>
)
@@ -65,6 +65,7 @@ export const createInitialPage = (name: string): Page => ({
angle: 135,
},
backgroundImageUrl: "",
backgroundVideoUrl: "",
});
export const getDefaultPageName = (index: number): string =>
+5
View File
@@ -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;
+3 -1
View File
@@ -119,10 +119,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 +135,7 @@ export type DocumentSettings = {
backgroundColor: string;
backgroundGradient?: LinearGradient;
backgroundImageUrl: string;
backgroundVideoUrl: string;
};
/** تصویر آپلودشده در گالری کاتالوگ (مشترک بین همهٔ صفحات) */
+26 -2
View File
@@ -48,7 +48,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 +56,7 @@ type BookPageProps = {
angle: number;
};
backgroundImageUrl?: string;
backgroundVideoUrl?: string;
/** فاز انیمیشن ورود — از BookViewer کنترل می‌شود */
entrancePhase?: EntrancePhase;
/** در پیش‌نمایش کاتالوگ انیمی션 ورود غیرفعال باشد */
@@ -82,7 +83,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 دارد
@@ -933,6 +934,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,6 +970,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
}}
>
{!useHardPage && (
<>
<div
aria-hidden
style={{
@@ -978,6 +981,27 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
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))}
+4 -2
View File
@@ -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}
+2 -1
View File
@@ -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;
};
+3 -1
View File
@@ -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,
};
});
}
+3 -1
View File
@@ -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 ?? "",
};
}
+4
View File
@@ -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());