This commit is contained in:
+30
-2
@@ -159,7 +159,7 @@ html {
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* سایهٔ هر ورق — فقط inset؛ بدون --simple تا موقع ورق هم بماند */
|
/* سایهٔ هر ورق — inset؛ بدون --simple تا موقع ورق هم بماند */
|
||||||
.flipbook-container .stf__item.--left .page {
|
.flipbook-container .stf__item.--left .page {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset -16px 0 24px -12px rgba(0, 0, 0, 0.2),
|
inset -16px 0 24px -12px rgba(0, 0, 0, 0.2),
|
||||||
@@ -193,7 +193,6 @@ html {
|
|||||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0.04) 35%, transparent 100%);
|
background: linear-gradient(to right, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0.04) 35%, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* تکصفحه (portrait): سایهٔ لبهٔ ورقزدن */
|
|
||||||
.flipbook-container .stf__wrapper.--portrait .stf__item .page {
|
.flipbook-container .stf__wrapper.--portrait .stf__item .page {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 14px 0 22px -12px rgba(0, 0, 0, 0.16),
|
inset 14px 0 22px -12px rgba(0, 0, 0, 0.16),
|
||||||
@@ -226,6 +225,35 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* iOS قدیمی: inset box-shadow و contain/isolate خراب میشود */
|
||||||
|
.flipbook-container.flipbook-legacy-ios .stf__item {
|
||||||
|
contain: none !important;
|
||||||
|
isolation: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipbook-container.flipbook-legacy-ios .stf__wrapper {
|
||||||
|
isolation: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipbook-container.flipbook-legacy-ios .stf__item .page {
|
||||||
|
isolation: auto;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipbook-container.flipbook-legacy-ios .stf__item .page::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipbook-container.flipbook-legacy-ios .page .page-paper-shadow {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.06) 16%, transparent 34%),
|
||||||
|
linear-gradient(to left, rgba(0, 0, 0, 0.12) 0%, rgba(0, 0, 0, 0.04) 14%, transparent 28%);
|
||||||
|
}
|
||||||
|
|
||||||
/* مخفی کردن محتوای صفحات غیرفعال - فقط صفحه فعلی قابل تعامل است */
|
/* مخفی کردن محتوای صفحات غیرفعال - فقط صفحه فعلی قابل تعامل است */
|
||||||
.flipbook-container .stf__wrapper {
|
.flipbook-container .stf__wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const CatalogPreview: FC<Props> = ({ item, page, size, className, selected }) =>
|
|||||||
pageWidth={pageWidth}
|
pageWidth={pageWidth}
|
||||||
pageHeight={pageHeight}
|
pageHeight={pageHeight}
|
||||||
disableEntranceAnimations
|
disableEntranceAnimations
|
||||||
|
showPaperShadow={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { type FC, useEffect, useState } from 'react';
|
import { type FC, useEffect, useState } from 'react';
|
||||||
import BookViewer from './components/BookViewer';
|
import BookViewer from './components/BookViewer';
|
||||||
|
import ViewerLoading from './components/ViewerLoading';
|
||||||
import { transformViewerDataToPages } from './utils/dataTransformer';
|
import { transformViewerDataToPages } from './utils/dataTransformer';
|
||||||
import type { PageData } from './types';
|
import type { PageData } from './types';
|
||||||
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useGetCatalogById } from '@/pages/home/hooks/useHomeData';
|
import { useGetCatalogById } from '@/pages/home/hooks/useHomeData';
|
||||||
import { useBrowserFullscreen } from './hooks/useBrowserFullscreen';
|
import { useBrowserFullscreen } from './hooks/useBrowserFullscreen';
|
||||||
|
import { useInitialPagesPreload } from './hooks/useInitialPagesPreload';
|
||||||
|
|
||||||
const Viewer: FC = () => {
|
const Viewer: FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -32,6 +34,10 @@ const Viewer: FC = () => {
|
|||||||
}, [data?.data?.content, id]);
|
}, [data?.data?.content, id]);
|
||||||
|
|
||||||
useBrowserFullscreen({ enabled: Boolean(id) });
|
useBrowserFullscreen({ enabled: Boolean(id) });
|
||||||
|
const { isReady: areInitialPagesReady, progress: preloadProgress } = useInitialPagesPreload(
|
||||||
|
pages,
|
||||||
|
documentSettings,
|
||||||
|
);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return (
|
return (
|
||||||
@@ -42,11 +48,7 @@ const Viewer: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <ViewerLoading />;
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<div className="text-gray-600">در حال بارگذاری...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryError) {
|
if (queryError) {
|
||||||
@@ -65,6 +67,10 @@ const Viewer: FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!areInitialPagesReady) {
|
||||||
|
return <ViewerLoading progress={preloadProgress} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full min-h-0 flex flex-col">
|
<div className="w-full h-full min-h-0 flex flex-col">
|
||||||
<BookViewer pages={pages} catalogSize={data?.data?.size} documentSettings={documentSettings} />
|
<BookViewer pages={pages} catalogSize={data?.data?.size} documentSettings={documentSettings} />
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ type BookPageProps = {
|
|||||||
entrancePhase?: EntrancePhase;
|
entrancePhase?: EntrancePhase;
|
||||||
/** در پیشنمایش کاتالوگ انیمیشن ورود غیرفعال باشد */
|
/** در پیشنمایش کاتالوگ انیمیشن ورود غیرفعال باشد */
|
||||||
disableEntranceAnimations?: boolean;
|
disableEntranceAnimations?: boolean;
|
||||||
|
/** سایهٔ لبهٔ کاغذ (گرادیان داخل DOM — سازگار با iOS قدیمی) */
|
||||||
|
showPaperShadow?: boolean;
|
||||||
|
/** ورق سخت بهجای clip-path برای iOS قدیمی */
|
||||||
|
useHardPage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +44,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 }, ref) => {
|
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, showPaperShadow = true, useHardPage = false }, 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 دارد
|
||||||
@@ -646,17 +650,21 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
className="page"
|
className="page"
|
||||||
data-density="soft"
|
data-density={useHardPage ? 'hard' : 'soft'}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
isolation: 'isolate',
|
isolation: useHardPage ? undefined : 'isolate',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
...(useHardPage
|
||||||
|
? { backgroundColor: effectiveBackgroundColor, ...bgStyle }
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!useHardPage && (
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
style={{
|
style={{
|
||||||
@@ -667,9 +675,11 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||||
{page.elements.map((element, index) => renderObject(element, index))}
|
{page.elements.map((element, index) => renderObject(element, index))}
|
||||||
</div>
|
</div>
|
||||||
|
{useHardPage && showPaperShadow && <div aria-hidden className="page-paper-shadow" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { useBookEntranceController } from '@/pages/viewer/hooks/useBookEntranceC
|
|||||||
import { useViewerViewport } from '@/pages/viewer/hooks/useViewerViewport';
|
import { useViewerViewport } from '@/pages/viewer/hooks/useViewerViewport';
|
||||||
import { useIsBrowserFullscreen } from '@/pages/viewer/hooks/useIsBrowserFullscreen';
|
import { useIsBrowserFullscreen } from '@/pages/viewer/hooks/useIsBrowserFullscreen';
|
||||||
import { toggleViewerFullscreen } from '@/pages/viewer/utils/viewerFullscreen';
|
import { toggleViewerFullscreen } from '@/pages/viewer/utils/viewerFullscreen';
|
||||||
|
import { isLegacyIOSSafari } from '@/pages/viewer/utils/isLegacyIOSSafari';
|
||||||
|
import { clx } from '@/helpers/utils';
|
||||||
|
|
||||||
/** فاصله پیشفرض بین هر ورق در پخش خودکار (میلیثانیه) */
|
/** فاصله پیشفرض بین هر ورق در پخش خودکار (میلیثانیه) */
|
||||||
const AUTO_PLAY_INTERVAL_MS = 3000;
|
const AUTO_PLAY_INTERVAL_MS = 3000;
|
||||||
@@ -36,6 +38,14 @@ function useIsDesktop() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useLegacyIOSSafari() {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
() => () => {},
|
||||||
|
isLegacyIOSSafari,
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type BookViewerProps = {
|
export type BookViewerProps = {
|
||||||
pages: PageData[];
|
pages: PageData[];
|
||||||
catalogSize?: string;
|
catalogSize?: string;
|
||||||
@@ -117,6 +127,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
const [isAutoPlayActive, setIsAutoPlayActive] = useState(false);
|
const [isAutoPlayActive, setIsAutoPlayActive] = useState(false);
|
||||||
const [startPage, setStartPage] = useState(0);
|
const [startPage, setStartPage] = useState(0);
|
||||||
const isDesktop = useIsDesktop();
|
const isDesktop = useIsDesktop();
|
||||||
|
const legacyIOS = useLegacyIOSSafari();
|
||||||
const isBrowserFullscreen = useIsBrowserFullscreen();
|
const isBrowserFullscreen = useIsBrowserFullscreen();
|
||||||
|
|
||||||
const autoPlay = documentSettings?.autoPlay ?? false;
|
const autoPlay = documentSettings?.autoPlay ?? false;
|
||||||
@@ -481,12 +492,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
minHeight={pagePixelHeight}
|
minHeight={pagePixelHeight}
|
||||||
maxWidth={pagePixelWidth}
|
maxWidth={pagePixelWidth}
|
||||||
maxHeight={pagePixelHeight}
|
maxHeight={pagePixelHeight}
|
||||||
drawShadow={true}
|
drawShadow={!legacyIOS}
|
||||||
flippingTime={800}
|
flippingTime={800}
|
||||||
usePortrait={usePortrait}
|
usePortrait={usePortrait}
|
||||||
startPage={startPage}
|
startPage={startPage}
|
||||||
autoSize={false}
|
autoSize={false}
|
||||||
maxShadowOpacity={isDesktop ? 0.55 : 0.32}
|
maxShadowOpacity={legacyIOS ? 0 : isDesktop ? 0.55 : 0.32}
|
||||||
showCover={hasMultiplePages}
|
showCover={hasMultiplePages}
|
||||||
mobileScrollSupport={!usePortrait}
|
mobileScrollSupport={!usePortrait}
|
||||||
clickEventForward={true}
|
clickEventForward={true}
|
||||||
@@ -495,7 +506,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
startZIndex={1}
|
startZIndex={1}
|
||||||
showPageCorners={hasMultiplePages}
|
showPageCorners={hasMultiplePages}
|
||||||
disableFlipByClick={true}
|
disableFlipByClick={true}
|
||||||
className="flipbook-container mx-auto max-w-full"
|
className={clx('flipbook-container mx-auto max-w-full', legacyIOS && 'flipbook-legacy-ios')}
|
||||||
style={{}}
|
style={{}}
|
||||||
onFlip={onFlipForwarded}
|
onFlip={onFlipForwarded}
|
||||||
onInit={handleBookInit}
|
onInit={handleBookInit}
|
||||||
@@ -514,6 +525,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
backgroundColor={page.backgroundColor ?? backgroundColor}
|
backgroundColor={page.backgroundColor ?? backgroundColor}
|
||||||
backgroundGradient={page.backgroundGradient ?? backgroundGradient}
|
backgroundGradient={page.backgroundGradient ?? backgroundGradient}
|
||||||
backgroundImageUrl={page.backgroundImageUrl ?? backgroundImageUrl}
|
backgroundImageUrl={page.backgroundImageUrl ?? backgroundImageUrl}
|
||||||
|
useHardPage={legacyIOS}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</HTMLFlipBook>
|
</HTMLFlipBook>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { type FC } from 'react';
|
||||||
|
|
||||||
|
type ViewerLoadingProps = {
|
||||||
|
progress?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewerLoading: FC<ViewerLoadingProps> = ({ progress }) => (
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||||
|
<div
|
||||||
|
className="size-10 rounded-full border-[3px] border-gray-200 border-t-gray-600 animate-spin"
|
||||||
|
role="status"
|
||||||
|
aria-label="در حال بارگذاری"
|
||||||
|
/>
|
||||||
|
<div className="text-gray-600 text-sm">در حال بارگذاری صفحات...</div>
|
||||||
|
{typeof progress === 'number' && progress > 0 && progress < 100 && (
|
||||||
|
<div className="text-xs text-gray-400">{progress}٪</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ViewerLoading;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
||||||
|
import type { PageData } from '../types';
|
||||||
|
import {
|
||||||
|
INITIAL_PAGES_PRELOAD_COUNT,
|
||||||
|
preloadPagesMedia,
|
||||||
|
} from '../utils/pageMediaUrls';
|
||||||
|
|
||||||
|
export function useInitialPagesPreload(
|
||||||
|
pages: PageData[],
|
||||||
|
documentSettings?: Partial<DocumentSettings>,
|
||||||
|
) {
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pages.length === 0) {
|
||||||
|
setIsReady(false);
|
||||||
|
setProgress(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setIsReady(false);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
const pagesToPreload = pages.slice(
|
||||||
|
0,
|
||||||
|
Math.min(INITIAL_PAGES_PRELOAD_COUNT, pages.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
void preloadPagesMedia(pagesToPreload, documentSettings, (loaded, total) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setProgress(total === 0 ? 100 : Math.round((loaded / total) * 100));
|
||||||
|
}).then(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setProgress(100);
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [pages, documentSettings]);
|
||||||
|
|
||||||
|
return { isReady, progress };
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/** iOS 14 و پایینتر: clip-path، inset box-shadow و contain/isolate باگ دارند */
|
||||||
|
export function isLegacyIOSSafari(): boolean {
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
const isIOS =
|
||||||
|
/iPad|iPhone|iPod/.test(ua) ||
|
||||||
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||||
|
|
||||||
|
if (!isIOS) return false;
|
||||||
|
|
||||||
|
const osMatch = ua.match(/OS (\d+)[_.](\d+)/);
|
||||||
|
if (osMatch) {
|
||||||
|
return parseInt(osMatch[1], 10) <= 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
||||||
|
import type { PageData } from '../types';
|
||||||
|
|
||||||
|
export const INITIAL_PAGES_PRELOAD_COUNT = 5;
|
||||||
|
|
||||||
|
type MediaAsset = {
|
||||||
|
url: string;
|
||||||
|
kind: 'image' | 'video';
|
||||||
|
};
|
||||||
|
|
||||||
|
const MEDIA_LOAD_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
function addImageUrl(urls: Map<string, MediaAsset>, url?: string) {
|
||||||
|
if (!url) return;
|
||||||
|
urls.set(url, { url, kind: 'image' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVideoUrl(urls: Map<string, MediaAsset>, url?: string) {
|
||||||
|
if (!url) return;
|
||||||
|
urls.set(url, { url, kind: 'video' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPageMediaAssets(
|
||||||
|
pages: PageData[],
|
||||||
|
documentSettings?: Partial<DocumentSettings>,
|
||||||
|
): MediaAsset[] {
|
||||||
|
const assets = new Map<string, MediaAsset>();
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.backgroundType === 'image') {
|
||||||
|
addImageUrl(assets, page.backgroundImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of page.elements) {
|
||||||
|
if (element.visible === false) continue;
|
||||||
|
|
||||||
|
if (element.type === 'image' || element.type === 'sticker') {
|
||||||
|
addImageUrl(assets, element.imageUrl);
|
||||||
|
} else if (element.type === 'video') {
|
||||||
|
addVideoUrl(assets, element.videoUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentSettings?.backgroundType === 'image') {
|
||||||
|
addImageUrl(assets, documentSettings.backgroundImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(assets.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadImage(url: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
const finish = () => {
|
||||||
|
img.onload = null;
|
||||||
|
img.onerror = null;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.onload = finish;
|
||||||
|
img.onerror = finish;
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadVideo(url: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.preload = 'auto';
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
video.onloadeddata = null;
|
||||||
|
video.onerror = null;
|
||||||
|
video.removeAttribute('src');
|
||||||
|
video.load();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
video.onloadeddata = finish;
|
||||||
|
video.onerror = finish;
|
||||||
|
video.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout(promise: Promise<void>, ms: number): Promise<void> {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
window.setTimeout(resolve, ms);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preloadMediaAsset(asset: MediaAsset): Promise<void> {
|
||||||
|
const load =
|
||||||
|
asset.kind === 'video' ? preloadVideo(asset.url) : preloadImage(asset.url);
|
||||||
|
return withTimeout(load, MEDIA_LOAD_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadPagesMedia(
|
||||||
|
pages: PageData[],
|
||||||
|
documentSettings?: Partial<DocumentSettings>,
|
||||||
|
onProgress?: (loaded: number, total: number) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
const assets = extractPageMediaAssets(pages, documentSettings);
|
||||||
|
const total = assets.length;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
onProgress?.(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loaded = 0;
|
||||||
|
await Promise.all(
|
||||||
|
assets.map(async (asset) => {
|
||||||
|
await preloadMediaAsset(asset);
|
||||||
|
loaded += 1;
|
||||||
|
onProgress?.(loaded, total);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user