fixed imports issue
This commit is contained in:
parent
0a36a87cd4
commit
ad9c788b21
2
frontend/next-env.d.ts
vendored
2
frontend/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
/// <reference path="./build/types/routes.d.ts" />
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -25,8 +25,14 @@ const PageSelector: React.FC<PageSelectorProps> = ({
|
|||||||
const sortedPages = useMemo(() => {
|
const sortedPages = useMemo(() => {
|
||||||
return [...pages].sort((a, b) => {
|
return [...pages].sort((a, b) => {
|
||||||
// Primary sort: sort_order ascending (undefined/null goes to end)
|
// Primary sort: sort_order ascending (undefined/null goes to end)
|
||||||
const orderA = typeof a.sort_order === 'number' ? a.sort_order : Number.MAX_SAFE_INTEGER;
|
const orderA =
|
||||||
const orderB = typeof b.sort_order === 'number' ? b.sort_order : Number.MAX_SAFE_INTEGER;
|
typeof a.sort_order === 'number'
|
||||||
|
? a.sort_order
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
const orderB =
|
||||||
|
typeof b.sort_order === 'number'
|
||||||
|
? b.sort_order
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
if (orderA !== orderB) {
|
if (orderA !== orderB) {
|
||||||
return orderA - orderB;
|
return orderA - orderB;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,15 +27,15 @@ export const RotatePrompt: React.FC<RotatePromptProps> = ({
|
|||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90">
|
<div className='fixed inset-0 z-[100] flex items-center justify-center bg-black/90'>
|
||||||
<div className="text-center text-white px-8">
|
<div className='text-center text-white px-8'>
|
||||||
<Icon
|
<Icon
|
||||||
path={mdiScreenRotation}
|
path={mdiScreenRotation}
|
||||||
size={3}
|
size={3}
|
||||||
className="mx-auto mb-6 animate-pulse"
|
className='mx-auto mb-6 animate-pulse'
|
||||||
/>
|
/>
|
||||||
<p className="text-lg font-medium">{message}</p>
|
<p className='text-lg font-medium'>{message}</p>
|
||||||
<p className="text-sm text-gray-400 mt-2">
|
<p className='text-sm text-gray-400 mt-2'>
|
||||||
This presentation is optimized for landscape viewing
|
This presentation is optimized for landscape viewing
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export default function RuntimePresentation({
|
|||||||
useCanvasScale({
|
useCanvasScale({
|
||||||
designWidth: currentPage?.design_width ?? undefined,
|
designWidth: currentPage?.design_width ?? undefined,
|
||||||
designHeight: currentPage?.design_height ?? undefined,
|
designHeight: currentPage?.design_height ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [transitionPreview, setTransitionPreview] = useState<{
|
const [transitionPreview, setTransitionPreview] = useState<{
|
||||||
targetPageId: string;
|
targetPageId: string;
|
||||||
@ -542,186 +542,193 @@ export default function RuntimePresentation({
|
|||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackdropPortalProvider>
|
<BackdropPortalProvider>
|
||||||
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
||||||
CSS backgroundImage provides instant display.
|
CSS backgroundImage provides instant display.
|
||||||
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
||||||
{backgroundImageUrl && !backgroundVideoUrl && (
|
{backgroundImageUrl && !backgroundVideoUrl && (
|
||||||
<div className='absolute inset-0 z-1 pointer-events-none'>
|
<div className='absolute inset-0 z-1 pointer-events-none'>
|
||||||
{backgroundImageUrl.startsWith('blob:') ? (
|
{backgroundImageUrl.startsWith('blob:') ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
key={backgroundImageUrl}
|
key={backgroundImageUrl}
|
||||||
src={backgroundImageUrl}
|
src={backgroundImageUrl}
|
||||||
alt=''
|
alt=''
|
||||||
className='absolute inset-0 w-full h-full object-contain'
|
className='absolute inset-0 w-full h-full object-contain'
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setIsBackgroundReady(true);
|
setIsBackgroundReady(true);
|
||||||
pageSwitch.markBackgroundReady();
|
pageSwitch.markBackgroundReady();
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setIsBackgroundReady(true);
|
setIsBackgroundReady(true);
|
||||||
pageSwitch.markBackgroundReady();
|
pageSwitch.markBackgroundReady();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
key={backgroundImageUrl}
|
key={backgroundImageUrl}
|
||||||
src={backgroundImageUrl}
|
src={backgroundImageUrl}
|
||||||
alt=''
|
alt=''
|
||||||
fill
|
fill
|
||||||
sizes='100vw'
|
sizes='100vw'
|
||||||
className='object-contain'
|
className='object-contain'
|
||||||
priority
|
priority
|
||||||
unoptimized
|
unoptimized
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setIsBackgroundReady(true);
|
setIsBackgroundReady(true);
|
||||||
pageSwitch.markBackgroundReady();
|
pageSwitch.markBackgroundReady();
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setIsBackgroundReady(true);
|
setIsBackgroundReady(true);
|
||||||
pageSwitch.markBackgroundReady();
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Previous background overlay - shows during direct navigation until new bg is ready */}
|
||||||
|
{pageSwitch.previousBgImageUrl &&
|
||||||
|
pageSwitch.isSwitching &&
|
||||||
|
!pageSwitch.isNewBgReady && (
|
||||||
|
<div
|
||||||
|
className='absolute inset-0 pointer-events-none z-10'
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
||||||
|
backgroundSize: 'contain',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Previous background overlay - shows during direct navigation until new bg is ready */}
|
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
||||||
{pageSwitch.previousBgImageUrl &&
|
{backgroundVideoUrl && (
|
||||||
pageSwitch.isSwitching &&
|
<video
|
||||||
!pageSwitch.isNewBgReady && (
|
ref={bgVideoRef}
|
||||||
<div
|
key={backgroundVideoUrl}
|
||||||
className='absolute inset-0 pointer-events-none z-10'
|
className='absolute inset-0 z-1 h-full w-full object-contain'
|
||||||
style={{
|
src={backgroundVideoUrl}
|
||||||
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
autoPlay={videoAutoplay}
|
||||||
backgroundSize: 'contain',
|
loop={useNativeLoop}
|
||||||
backgroundPosition: 'center',
|
muted={videoMuted}
|
||||||
backgroundRepeat: 'no-repeat',
|
playsInline
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
||||||
{backgroundVideoUrl && (
|
<div
|
||||||
<video
|
className='absolute inset-0 z-40'
|
||||||
ref={bgVideoRef}
|
style={{
|
||||||
key={backgroundVideoUrl}
|
opacity: elementsOpacity,
|
||||||
className='absolute inset-0 z-1 h-full w-full object-contain'
|
transition: isFadingIn
|
||||||
src={backgroundVideoUrl}
|
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
||||||
autoPlay={videoAutoplay}
|
: 'none',
|
||||||
loop={useNativeLoop}
|
}}
|
||||||
muted={videoMuted}
|
>
|
||||||
playsInline
|
{pageElements.map((element: CanvasElement) => (
|
||||||
/>
|
<RuntimeElement
|
||||||
)}
|
key={element.id}
|
||||||
|
element={element}
|
||||||
|
onClick={() => handleElementClick(element)}
|
||||||
|
resolveUrl={resolveUrlWithBlob}
|
||||||
|
onGalleryCardClick={(cardIndex) =>
|
||||||
|
handleGalleryCardClick(element, cardIndex)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
{/* Controls: Offline toggle and Fullscreen button */}
|
||||||
<div
|
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
||||||
className='absolute inset-0 z-40'
|
<OfflineToggle
|
||||||
style={{
|
projectId={project?.id || null}
|
||||||
opacity: elementsOpacity,
|
projectSlug={projectSlug}
|
||||||
transition: isFadingIn
|
projectName={project?.name}
|
||||||
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
pages={pages}
|
||||||
: 'none',
|
showLabel={false}
|
||||||
}}
|
size='small'
|
||||||
>
|
|
||||||
{pageElements.map((element: CanvasElement) => (
|
|
||||||
<RuntimeElement
|
|
||||||
key={element.id}
|
|
||||||
element={element}
|
|
||||||
onClick={() => handleElementClick(element)}
|
|
||||||
resolveUrl={resolveUrlWithBlob}
|
|
||||||
onGalleryCardClick={(cardIndex) =>
|
|
||||||
handleGalleryCardClick(element, cardIndex)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
<BaseButton
|
||||||
</div>
|
icon={isFullscreen ? mdiFullscreenExit : mdiFullscreen}
|
||||||
|
color='info'
|
||||||
{/* Controls: Offline toggle and Fullscreen button */}
|
small
|
||||||
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
onClick={toggleFullscreen}
|
||||||
<OfflineToggle
|
|
||||||
projectId={project?.id || null}
|
|
||||||
projectSlug={projectSlug}
|
|
||||||
projectName={project?.name}
|
|
||||||
pages={pages}
|
|
||||||
showLabel={false}
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
icon={isFullscreen ? mdiFullscreenExit : mdiFullscreen}
|
|
||||||
color='info'
|
|
||||||
small
|
|
||||||
onClick={toggleFullscreen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
|
||||||
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
|
||||||
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
|
||||||
{transitionPreview && (
|
|
||||||
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
|
||||||
<video
|
|
||||||
ref={transitionVideoRef}
|
|
||||||
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
|
||||||
style={{
|
|
||||||
opacity:
|
|
||||||
transitionPhase === 'preparing' ||
|
|
||||||
isBuffering ||
|
|
||||||
isOverlayFadingOut
|
|
||||||
? 0
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
preload='auto'
|
|
||||||
disablePictureInPicture
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Gallery Carousel Overlay */}
|
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||||
{activeGalleryCarousel && (
|
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
||||||
<GalleryCarouselOverlay
|
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
||||||
cards={activeGalleryCarousel.element.galleryCards || []}
|
{transitionPreview && (
|
||||||
initialIndex={activeGalleryCarousel.initialIndex}
|
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
||||||
onClose={() => setActiveGalleryCarousel(null)}
|
<video
|
||||||
resolveUrl={resolveUrlWithBlob}
|
ref={transitionVideoRef}
|
||||||
prevIconUrl={
|
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
||||||
activeGalleryCarousel.element.galleryCarouselPrevIconUrl
|
style={{
|
||||||
}
|
opacity:
|
||||||
nextIconUrl={
|
transitionPhase === 'preparing' ||
|
||||||
activeGalleryCarousel.element.galleryCarouselNextIconUrl
|
isBuffering ||
|
||||||
}
|
isOverlayFadingOut
|
||||||
backIconUrl={
|
? 0
|
||||||
activeGalleryCarousel.element.galleryCarouselBackIconUrl
|
: 1,
|
||||||
}
|
}}
|
||||||
backLabel={
|
muted
|
||||||
activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK'
|
playsInline
|
||||||
}
|
preload='auto'
|
||||||
prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
|
disablePictureInPicture
|
||||||
prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
|
/>
|
||||||
nextX={activeGalleryCarousel.element.galleryCarouselNextX}
|
</div>
|
||||||
nextY={activeGalleryCarousel.element.galleryCarouselNextY}
|
)}
|
||||||
backX={activeGalleryCarousel.element.galleryCarouselBackX}
|
|
||||||
backY={activeGalleryCarousel.element.galleryCarouselBackY}
|
{/* Gallery Carousel Overlay */}
|
||||||
prevWidth={activeGalleryCarousel.element.galleryCarouselPrevWidth}
|
{activeGalleryCarousel && (
|
||||||
prevHeight={
|
<GalleryCarouselOverlay
|
||||||
activeGalleryCarousel.element.galleryCarouselPrevHeight
|
cards={activeGalleryCarousel.element.galleryCards || []}
|
||||||
}
|
initialIndex={activeGalleryCarousel.initialIndex}
|
||||||
nextWidth={activeGalleryCarousel.element.galleryCarouselNextWidth}
|
onClose={() => setActiveGalleryCarousel(null)}
|
||||||
nextHeight={
|
resolveUrl={resolveUrlWithBlob}
|
||||||
activeGalleryCarousel.element.galleryCarouselNextHeight
|
prevIconUrl={
|
||||||
}
|
activeGalleryCarousel.element.galleryCarouselPrevIconUrl
|
||||||
backWidth={activeGalleryCarousel.element.galleryCarouselBackWidth}
|
}
|
||||||
backHeight={
|
nextIconUrl={
|
||||||
activeGalleryCarousel.element.galleryCarouselBackHeight
|
activeGalleryCarousel.element.galleryCarouselNextIconUrl
|
||||||
}
|
}
|
||||||
isEditMode={false}
|
backIconUrl={
|
||||||
/>
|
activeGalleryCarousel.element.galleryCarouselBackIconUrl
|
||||||
)}
|
}
|
||||||
</BackdropPortalProvider>
|
backLabel={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselBackLabel ||
|
||||||
|
'BACK'
|
||||||
|
}
|
||||||
|
prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
|
||||||
|
prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
|
||||||
|
nextX={activeGalleryCarousel.element.galleryCarouselNextX}
|
||||||
|
nextY={activeGalleryCarousel.element.galleryCarouselNextY}
|
||||||
|
backX={activeGalleryCarousel.element.galleryCarouselBackX}
|
||||||
|
backY={activeGalleryCarousel.element.galleryCarouselBackY}
|
||||||
|
prevWidth={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselPrevWidth
|
||||||
|
}
|
||||||
|
prevHeight={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselPrevHeight
|
||||||
|
}
|
||||||
|
nextWidth={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselNextWidth
|
||||||
|
}
|
||||||
|
nextHeight={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselNextHeight
|
||||||
|
}
|
||||||
|
backWidth={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselBackWidth
|
||||||
|
}
|
||||||
|
backHeight={
|
||||||
|
activeGalleryCarousel.element.galleryCarouselBackHeight
|
||||||
|
}
|
||||||
|
isEditMode={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</BackdropPortalProvider>
|
||||||
</div>
|
</div>
|
||||||
{/* End inner canvas container */}
|
{/* End inner canvas container */}
|
||||||
|
|
||||||
|
|||||||
@ -248,7 +248,8 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
|
|||||||
if (!value || value.trim() === '') return undefined;
|
if (!value || value.trim() === '') return undefined;
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
// If value already uses canvas units or calc, preserve it
|
// If value already uses canvas units or calc, preserve it
|
||||||
if (trimmed.includes('var(--cu') || trimmed.includes('--cu')) return trimmed;
|
if (trimmed.includes('var(--cu') || trimmed.includes('--cu'))
|
||||||
|
return trimmed;
|
||||||
if (/^calc\(/i.test(trimmed)) return trimmed;
|
if (/^calc\(/i.test(trimmed)) return trimmed;
|
||||||
// If value already has other units, convert them
|
// If value already has other units, convert them
|
||||||
const vwMatch = trimmed.match(/^(-?\d*\.?\d+)vw$/i);
|
const vwMatch = trimmed.match(/^(-?\d*\.?\d+)vw$/i);
|
||||||
|
|||||||
@ -1,217 +0,0 @@
|
|||||||
/**
|
|
||||||
* useBackgroundDimensionSuggestion Hook
|
|
||||||
*
|
|
||||||
* Detects background media dimensions and suggests canvas size updates.
|
|
||||||
* Used in constructor to prompt user when background resolution differs from project settings.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useCallback, useState, useRef } from 'react';
|
|
||||||
import { logger } from '../lib/logger';
|
|
||||||
|
|
||||||
interface MediaDimensions {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseBackgroundDimensionSuggestionOptions {
|
|
||||||
/** URL of the background media (image or video) */
|
|
||||||
mediaUrl: string | undefined;
|
|
||||||
/** Current project design width */
|
|
||||||
currentDesignWidth: number;
|
|
||||||
/** Current project design height */
|
|
||||||
currentDesignHeight: number;
|
|
||||||
/** Callback when dimensions differ from current settings */
|
|
||||||
onSuggest: (width: number, height: number) => void;
|
|
||||||
/** Whether suggestion is enabled */
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect dimensions of an image URL.
|
|
||||||
*/
|
|
||||||
async function getImageDimensions(
|
|
||||||
url: string,
|
|
||||||
): Promise<MediaDimensions | null> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
resolve({
|
|
||||||
width: img.naturalWidth,
|
|
||||||
height: img.naturalHeight,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect dimensions of a video URL.
|
|
||||||
*/
|
|
||||||
async function getVideoDimensions(
|
|
||||||
url: string,
|
|
||||||
): Promise<MediaDimensions | null> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const video = document.createElement('video');
|
|
||||||
video.preload = 'metadata';
|
|
||||||
|
|
||||||
video.onloadedmetadata = () => {
|
|
||||||
resolve({
|
|
||||||
width: video.videoWidth,
|
|
||||||
height: video.videoHeight,
|
|
||||||
});
|
|
||||||
// Clean up
|
|
||||||
video.src = '';
|
|
||||||
video.load();
|
|
||||||
};
|
|
||||||
|
|
||||||
video.onerror = () => {
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
video.src = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if URL is likely a video based on extension.
|
|
||||||
*/
|
|
||||||
function isVideoUrl(url: string): boolean {
|
|
||||||
const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'];
|
|
||||||
const lowercaseUrl = url.toLowerCase();
|
|
||||||
return videoExtensions.some((ext) => lowercaseUrl.includes(ext));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if URL is a resolved/loadable URL (not a raw storage path).
|
|
||||||
* Only process: http(s) URLs, blob URLs, data URLs
|
|
||||||
*/
|
|
||||||
function isLoadableUrl(url: string): boolean {
|
|
||||||
return (
|
|
||||||
url.startsWith('http://') ||
|
|
||||||
url.startsWith('https://') ||
|
|
||||||
url.startsWith('blob:') ||
|
|
||||||
url.startsWith('data:')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get media dimensions from URL (image or video).
|
|
||||||
*/
|
|
||||||
async function getMediaDimensions(
|
|
||||||
url: string,
|
|
||||||
): Promise<MediaDimensions | null> {
|
|
||||||
if (isVideoUrl(url)) {
|
|
||||||
return getVideoDimensions(url);
|
|
||||||
}
|
|
||||||
return getImageDimensions(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to detect background media dimensions and suggest canvas updates.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { suggestion, dismissSuggestion } = useBackgroundDimensionSuggestion({
|
|
||||||
* mediaUrl: backgroundImageUrl,
|
|
||||||
* currentDesignWidth: project.design_width ?? 1920,
|
|
||||||
* currentDesignHeight: project.design_height ?? 1080,
|
|
||||||
* onSuggest: (width, height) => {
|
|
||||||
* // Show suggestion UI
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useBackgroundDimensionSuggestion({
|
|
||||||
mediaUrl,
|
|
||||||
currentDesignWidth,
|
|
||||||
currentDesignHeight,
|
|
||||||
onSuggest,
|
|
||||||
enabled = true,
|
|
||||||
}: UseBackgroundDimensionSuggestionOptions) {
|
|
||||||
const [suggestion, setSuggestion] = useState<MediaDimensions | null>(null);
|
|
||||||
const lastCheckedUrlRef = useRef<string | null>(null);
|
|
||||||
const dismissedUrlsRef = useRef<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const dismissSuggestion = useCallback(() => {
|
|
||||||
if (mediaUrl) {
|
|
||||||
dismissedUrlsRef.current.add(mediaUrl);
|
|
||||||
}
|
|
||||||
setSuggestion(null);
|
|
||||||
}, [mediaUrl]);
|
|
||||||
|
|
||||||
const acceptSuggestion = useCallback(() => {
|
|
||||||
if (suggestion) {
|
|
||||||
onSuggest(suggestion.width, suggestion.height);
|
|
||||||
setSuggestion(null);
|
|
||||||
}
|
|
||||||
}, [suggestion, onSuggest]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled || !mediaUrl) {
|
|
||||||
setSuggestion(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip raw storage paths - only process resolved URLs (http, https, blob, data)
|
|
||||||
if (!isLoadableUrl(mediaUrl)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if already checked this URL
|
|
||||||
if (lastCheckedUrlRef.current === mediaUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if user dismissed this URL
|
|
||||||
if (dismissedUrlsRef.current.has(mediaUrl)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCheckedUrlRef.current = mediaUrl;
|
|
||||||
|
|
||||||
const detectDimensions = async () => {
|
|
||||||
try {
|
|
||||||
const dimensions = await getMediaDimensions(mediaUrl);
|
|
||||||
|
|
||||||
if (!dimensions) {
|
|
||||||
logger.warn('[CanvasSuggestion] Failed to detect media dimensions', {
|
|
||||||
url: mediaUrl,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if dimensions differ significantly (allow for small rounding)
|
|
||||||
const widthDiff = Math.abs(dimensions.width - currentDesignWidth);
|
|
||||||
const heightDiff = Math.abs(dimensions.height - currentDesignHeight);
|
|
||||||
const threshold = 10; // pixels
|
|
||||||
|
|
||||||
if (widthDiff > threshold || heightDiff > threshold) {
|
|
||||||
logger.info('[CanvasSuggestion] Detected different dimensions', {
|
|
||||||
media: dimensions,
|
|
||||||
current: { width: currentDesignWidth, height: currentDesignHeight },
|
|
||||||
});
|
|
||||||
setSuggestion(dimensions);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
'[CanvasSuggestion] Error detecting dimensions',
|
|
||||||
error instanceof Error ? error : { error },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void detectDimensions();
|
|
||||||
}, [mediaUrl, currentDesignWidth, currentDesignHeight, enabled]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
/** Suggested dimensions from detected media */
|
|
||||||
suggestion,
|
|
||||||
/** Dismiss the current suggestion */
|
|
||||||
dismissSuggestion,
|
|
||||||
/** Accept the suggestion and call onSuggest callback */
|
|
||||||
acceptSuggestion,
|
|
||||||
/** Whether a suggestion is currently available */
|
|
||||||
hasSuggestion: suggestion !== null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -53,7 +53,9 @@ interface CanvasScaleResult {
|
|||||||
*
|
*
|
||||||
* return <div style={cssVars}>{content}</div>;
|
* return <div style={cssVars}>{content}</div>;
|
||||||
*/
|
*/
|
||||||
export function useCanvasScale(options?: UseCanvasScaleOptions): CanvasScaleResult {
|
export function useCanvasScale(
|
||||||
|
options?: UseCanvasScaleOptions,
|
||||||
|
): CanvasScaleResult {
|
||||||
const designWidth = options?.designWidth ?? CANVAS_CONFIG.defaults.width;
|
const designWidth = options?.designWidth ?? CANVAS_CONFIG.defaults.width;
|
||||||
const designHeight = options?.designHeight ?? CANVAS_CONFIG.defaults.height;
|
const designHeight = options?.designHeight ?? CANVAS_CONFIG.defaults.height;
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,11 @@ const EMPTY_ASSETS: Asset[] = [];
|
|||||||
|
|
||||||
interface UseConstructorDataResult {
|
interface UseConstructorDataResult {
|
||||||
// Project
|
// Project
|
||||||
project: { name: string; design_width?: number; design_height?: number } | null;
|
project: {
|
||||||
|
name: string;
|
||||||
|
design_width?: number;
|
||||||
|
design_height?: number;
|
||||||
|
} | null;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
|||||||
@ -82,7 +82,13 @@ const formatBytes = (bytes: number): string => {
|
|||||||
export function useOfflineMode(
|
export function useOfflineMode(
|
||||||
options: UseOfflineModeOptions,
|
options: UseOfflineModeOptions,
|
||||||
): UseOfflineModeResult {
|
): UseOfflineModeResult {
|
||||||
const { projectId, projectSlug, projectName, pages, enabled = true } = options;
|
const {
|
||||||
|
projectId,
|
||||||
|
projectSlug,
|
||||||
|
projectName,
|
||||||
|
pages,
|
||||||
|
enabled = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const [projectInfo, setProjectInfo] = useState<OfflineProject | null>(null);
|
const [projectInfo, setProjectInfo] = useState<OfflineProject | null>(null);
|
||||||
const [discoveredAssets, setDiscoveredAssets] = useState<AssetToCache[]>([]);
|
const [discoveredAssets, setDiscoveredAssets] = useState<AssetToCache[]>([]);
|
||||||
@ -141,7 +147,9 @@ export function useOfflineMode(
|
|||||||
|
|
||||||
// Find the asset to get its size
|
// Find the asset to get its size
|
||||||
const asset = assetsRef.current.find(
|
const asset = assetsRef.current.find(
|
||||||
(a) => a.storageKey === data.storageKey || `offline-${a.storageKey}` === data.assetId,
|
(a) =>
|
||||||
|
a.storageKey === data.storageKey ||
|
||||||
|
`offline-${a.storageKey}` === data.assetId,
|
||||||
);
|
);
|
||||||
const assetSize = asset?.sizeBytes || 0;
|
const assetSize = asset?.sizeBytes || 0;
|
||||||
|
|
||||||
@ -152,7 +160,10 @@ export function useOfflineMode(
|
|||||||
const downloaded = downloadedCountRef.current;
|
const downloaded = downloadedCountRef.current;
|
||||||
const dlBytes = downloadedBytesRef.current;
|
const dlBytes = downloadedBytesRef.current;
|
||||||
const total = assetsRef.current.length;
|
const total = assetsRef.current.length;
|
||||||
const totalSize = assetsRef.current.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
|
const totalSize = assetsRef.current.reduce(
|
||||||
|
(sum, a) => sum + (a.sizeBytes || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
setDownloadedAssets(downloaded);
|
setDownloadedAssets(downloaded);
|
||||||
@ -266,7 +277,9 @@ export function useOfflineMode(
|
|||||||
// Discover assets from pages (frontend-only, no backend call)
|
// Discover assets from pages (frontend-only, no backend call)
|
||||||
const assets = discoverAssets();
|
const assets = discoverAssets();
|
||||||
if (assets.length === 0) {
|
if (assets.length === 0) {
|
||||||
throw new Error('No assets discovered. Make sure pages data is provided.');
|
throw new Error(
|
||||||
|
'No assets discovered. Make sure pages data is provided.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store assets for event-driven progress tracking
|
// Store assets for event-driven progress tracking
|
||||||
@ -275,7 +288,10 @@ export function useOfflineMode(
|
|||||||
setTotalAssets(assets.length);
|
setTotalAssets(assets.length);
|
||||||
|
|
||||||
// Estimate total size (may not have exact sizes for all assets)
|
// Estimate total size (may not have exact sizes for all assets)
|
||||||
const estimatedTotalSize = assets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
|
const estimatedTotalSize = assets.reduce(
|
||||||
|
(sum, a) => sum + (a.sizeBytes || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
setTotalBytes(estimatedTotalSize);
|
setTotalBytes(estimatedTotalSize);
|
||||||
|
|
||||||
// Create or update project record
|
// Create or update project record
|
||||||
@ -307,7 +323,8 @@ export function useOfflineMode(
|
|||||||
// Partial downloads from online preload need to be re-downloaded fully for offline
|
// Partial downloads from online preload need to be re-downloaded fully for offline
|
||||||
const assetsToDownload: AssetToCache[] = [];
|
const assetsToDownload: AssetToCache[] = [];
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
const assetInfo: CachedAssetInfo | null = await StorageManager.getAssetInfo(asset.storageKey);
|
const assetInfo: CachedAssetInfo | null =
|
||||||
|
await StorageManager.getAssetInfo(asset.storageKey);
|
||||||
|
|
||||||
if (assetInfo?.exists && !assetInfo.isPartial) {
|
if (assetInfo?.exists && !assetInfo.isPartial) {
|
||||||
// Fully cached, skip
|
// Fully cached, skip
|
||||||
@ -318,10 +335,13 @@ export function useOfflineMode(
|
|||||||
});
|
});
|
||||||
} else if (assetInfo?.exists && assetInfo.isPartial) {
|
} else if (assetInfo?.exists && assetInfo.isPartial) {
|
||||||
// Partial download - need full download for offline
|
// Partial download - need full download for offline
|
||||||
logger.info('[useOfflineMode] Upgrading partial download for offline', {
|
logger.info(
|
||||||
storageKey: asset.storageKey.slice(-50),
|
'[useOfflineMode] Upgrading partial download for offline',
|
||||||
partialSize: assetInfo.sizeBytes,
|
{
|
||||||
});
|
storageKey: asset.storageKey.slice(-50),
|
||||||
|
partialSize: assetInfo.sizeBytes,
|
||||||
|
},
|
||||||
|
);
|
||||||
assetsToDownload.push(asset);
|
assetsToDownload.push(asset);
|
||||||
} else {
|
} else {
|
||||||
// Not cached at all
|
// Not cached at all
|
||||||
@ -367,9 +387,12 @@ export function useOfflineMode(
|
|||||||
count: Object.keys(presignedUrls).length,
|
count: Object.keys(presignedUrls).length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('[useOfflineMode] Failed to fetch presigned URLs, using proxy', {
|
logger.warn(
|
||||||
error: err instanceof Error ? err.message : 'unknown',
|
'[useOfflineMode] Failed to fetch presigned URLs, using proxy',
|
||||||
});
|
{
|
||||||
|
error: err instanceof Error ? err.message : 'unknown',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +401,9 @@ export function useOfflineMode(
|
|||||||
// Progress is tracked via event subscriptions (see useEffect above)
|
// Progress is tracked via event subscriptions (see useEffect above)
|
||||||
for (const asset of assetsToDownload) {
|
for (const asset of assetsToDownload) {
|
||||||
// Resolve download URL - prefer presigned, fallback to proxy
|
// Resolve download URL - prefer presigned, fallback to proxy
|
||||||
const downloadUrl = presignedUrls[asset.storageKey] || resolveAssetPlaybackUrl(asset.storageKey);
|
const downloadUrl =
|
||||||
|
presignedUrls[asset.storageKey] ||
|
||||||
|
resolveAssetPlaybackUrl(asset.storageKey);
|
||||||
|
|
||||||
downloadManager
|
downloadManager
|
||||||
.addJob({
|
.addJob({
|
||||||
@ -491,7 +516,9 @@ export function useOfflineMode(
|
|||||||
// Computed values
|
// Computed values
|
||||||
const isDownloaded = status === 'downloaded';
|
const isDownloaded = status === 'downloaded';
|
||||||
const isDownloading = status === 'downloading' && !isPaused;
|
const isDownloading = status === 'downloading' && !isPaused;
|
||||||
const estimatedSize = discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) || totalBytes;
|
const estimatedSize =
|
||||||
|
discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) ||
|
||||||
|
totalBytes;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOfflineCapable,
|
isOfflineCapable,
|
||||||
|
|||||||
@ -33,7 +33,11 @@ import type {
|
|||||||
PreloadPageLink,
|
PreloadPageLink,
|
||||||
PreloadElement,
|
PreloadElement,
|
||||||
} from '../../types/preload';
|
} from '../../types/preload';
|
||||||
import type { AssetType, AssetVariantType, CachedAssetInfo } from '../../types/offline';
|
import type {
|
||||||
|
AssetType,
|
||||||
|
AssetVariantType,
|
||||||
|
CachedAssetInfo,
|
||||||
|
} from '../../types/offline';
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
export type { CachedAssetInfo };
|
export type { CachedAssetInfo };
|
||||||
@ -102,7 +106,9 @@ export class AssetCacheService {
|
|||||||
* Get cache status for a single asset
|
* Get cache status for a single asset
|
||||||
* Returns whether it exists, if it's partial, and size info
|
* Returns whether it exists, if it's partial, and size info
|
||||||
*/
|
*/
|
||||||
static async getAssetInfo(storageKey: string): Promise<CachedAssetInfo | null> {
|
static async getAssetInfo(
|
||||||
|
storageKey: string,
|
||||||
|
): Promise<CachedAssetInfo | null> {
|
||||||
return StorageManager.getAssetInfo(storageKey);
|
return StorageManager.getAssetInfo(storageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +231,8 @@ export class AssetCacheService {
|
|||||||
|
|
||||||
// Determine download parameters based on mode
|
// Determine download parameters based on mode
|
||||||
const isOnlineMode = mode === 'online';
|
const isOnlineMode = mode === 'online';
|
||||||
const isCurrentPageAsset = currentPageId && asset.pageId === currentPageId;
|
const isCurrentPageAsset =
|
||||||
|
currentPageId && asset.pageId === currentPageId;
|
||||||
|
|
||||||
// Online mode: use partial downloads for neighbor page media
|
// Online mode: use partial downloads for neighbor page media
|
||||||
// Offline mode: always full downloads
|
// Offline mode: always full downloads
|
||||||
|
|||||||
@ -47,10 +47,7 @@ const DEFAULT_OPTIONS: AssetDiscoveryOptions = {
|
|||||||
/**
|
/**
|
||||||
* Classify asset type based on field name
|
* Classify asset type based on field name
|
||||||
*/
|
*/
|
||||||
export function classifyAssetType(
|
export function classifyAssetType(fieldName: string, url: string): AssetType {
|
||||||
fieldName: string,
|
|
||||||
url: string,
|
|
||||||
): AssetType {
|
|
||||||
const lowerField = fieldName.toLowerCase();
|
const lowerField = fieldName.toLowerCase();
|
||||||
const lowerUrl = url.toLowerCase();
|
const lowerUrl = url.toLowerCase();
|
||||||
|
|
||||||
@ -331,7 +328,11 @@ export function getPrioritizedAssets(
|
|||||||
|
|
||||||
assets.push({
|
assets.push({
|
||||||
...asset,
|
...asset,
|
||||||
priority: calculateAssetPriority(asset.assetType, isCurrentPage, distance),
|
priority: calculateAssetPriority(
|
||||||
|
asset.assetType,
|
||||||
|
isCurrentPage,
|
||||||
|
distance,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -348,8 +349,8 @@ export function getPrioritizedAssets(
|
|||||||
const currentPageElements = elements.filter(
|
const currentPageElements = elements.filter(
|
||||||
(el) => el.pageId === currentPageId,
|
(el) => el.pageId === currentPageId,
|
||||||
);
|
);
|
||||||
extractElementAssets(currentPageElements, currentPageId).forEach((asset) =>
|
extractElementAssets(currentPageElements, currentPageId).forEach(
|
||||||
addAssetWithPriority(asset, true, 0),
|
(asset) => addAssetWithPriority(asset, true, 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +403,12 @@ export function toPreloadAssetInfo(asset: AssetToCache): PreloadAssetInfo {
|
|||||||
return {
|
return {
|
||||||
url: asset.originalUrl,
|
url: asset.originalUrl,
|
||||||
pageId: asset.pageId,
|
pageId: asset.pageId,
|
||||||
assetType: assetType as 'image' | 'video' | 'audio' | 'transition' | 'other',
|
assetType: assetType as
|
||||||
|
| 'image'
|
||||||
|
| 'video'
|
||||||
|
| 'audio'
|
||||||
|
| 'transition'
|
||||||
|
| 'other',
|
||||||
priority: asset.priority,
|
priority: asset.priority,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
* Unified asset caching for online preload and offline download.
|
* Unified asset caching for online preload and offline download.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { AssetCacheService, type DownloadProgress, type QueueDownloadOptions } from './AssetCacheService';
|
export {
|
||||||
|
AssetCacheService,
|
||||||
|
type DownloadProgress,
|
||||||
|
type QueueDownloadOptions,
|
||||||
|
} from './AssetCacheService';
|
||||||
export type { CachedAssetInfo } from '../../types/offline';
|
export type { CachedAssetInfo } from '../../types/offline';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -130,7 +130,13 @@ export function remToDesignPx(value: number): number {
|
|||||||
*/
|
*/
|
||||||
export function normalizeToCanvasUnits(
|
export function normalizeToCanvasUnits(
|
||||||
value: string | number | undefined,
|
value: string | number | undefined,
|
||||||
property: 'width' | 'height' | 'fontSize' | 'padding' | 'borderRadius' | 'gap',
|
property:
|
||||||
|
| 'width'
|
||||||
|
| 'height'
|
||||||
|
| 'fontSize'
|
||||||
|
| 'padding'
|
||||||
|
| 'borderRadius'
|
||||||
|
| 'gap',
|
||||||
designWidth: number = CANVAS_CONFIG.defaults.width,
|
designWidth: number = CANVAS_CONFIG.defaults.width,
|
||||||
designHeight: number = CANVAS_CONFIG.defaults.height,
|
designHeight: number = CANVAS_CONFIG.defaults.height,
|
||||||
): string {
|
): string {
|
||||||
|
|||||||
@ -417,7 +417,8 @@ export function buildGallerySpanGridStyle(
|
|||||||
element: Partial<CanvasElement>,
|
element: Partial<CanvasElement>,
|
||||||
): CSSProperties {
|
): CSSProperties {
|
||||||
const columns = getGalleryGridColumns(element, 'span');
|
const columns = getGalleryGridColumns(element, 'span');
|
||||||
const gap = normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
|
const gap =
|
||||||
|
normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -518,7 +519,8 @@ export function buildGalleryCardGridStyle(
|
|||||||
element: Partial<CanvasElement>,
|
element: Partial<CanvasElement>,
|
||||||
): CSSProperties {
|
): CSSProperties {
|
||||||
const columns = getGalleryGridColumns(element, 'card');
|
const columns = getGalleryGridColumns(element, 'card');
|
||||||
const gap = normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
|
const gap =
|
||||||
|
normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@ -92,9 +92,12 @@ class DownloadManagerClass {
|
|||||||
|
|
||||||
// For partial downloads, check session cache first (fast path)
|
// For partial downloads, check session cache first (fast path)
|
||||||
if (isPartialDownload && this.partialDownloadsReady.has(storageKey)) {
|
if (isPartialDownload && this.partialDownloadsReady.has(storageKey)) {
|
||||||
logger.info('[DownloadManager] Partial download already ready (session)', {
|
logger.info(
|
||||||
storageKey: storageKey.slice(-50),
|
'[DownloadManager] Partial download already ready (session)',
|
||||||
});
|
{
|
||||||
|
storageKey: storageKey.slice(-50),
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,10 +419,13 @@ class DownloadManagerClass {
|
|||||||
markPresignedUrlFailed(job.storageKey);
|
markPresignedUrlFailed(job.storageKey);
|
||||||
const proxyUrl = buildProxyUrl(job.storageKey);
|
const proxyUrl = buildProxyUrl(job.storageKey);
|
||||||
|
|
||||||
logger.info('[DownloadManager] Presigned URL failed, retrying with proxy', {
|
logger.info(
|
||||||
storageKey: job.storageKey.slice(-50),
|
'[DownloadManager] Presigned URL failed, retrying with proxy',
|
||||||
error: errorMessage,
|
{
|
||||||
});
|
storageKey: job.storageKey.slice(-50),
|
||||||
|
error: errorMessage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Update job to use proxy URL and retry immediately
|
// Update job to use proxy URL and retry immediately
|
||||||
job.url = proxyUrl;
|
job.url = proxyUrl;
|
||||||
@ -718,7 +724,10 @@ class DownloadManagerClass {
|
|||||||
* This enables the SW to cache the full response when the browser fetches the media
|
* This enables the SW to cache the full response when the browser fetches the media
|
||||||
* during playback, using the canonical storage key instead of the expiring presigned URL.
|
* during playback, using the canonical storage key instead of the expiring presigned URL.
|
||||||
*/
|
*/
|
||||||
private registerUrlForCaching(presignedUrl: string, storageKey: string): void {
|
private registerUrlForCaching(
|
||||||
|
presignedUrl: string,
|
||||||
|
storageKey: string,
|
||||||
|
): void {
|
||||||
if (navigator.serviceWorker?.controller) {
|
if (navigator.serviceWorker?.controller) {
|
||||||
navigator.serviceWorker.controller.postMessage({
|
navigator.serviceWorker.controller.postMessage({
|
||||||
type: 'REGISTER_CACHE_URL',
|
type: 'REGISTER_CACHE_URL',
|
||||||
|
|||||||
@ -86,9 +86,7 @@ import {
|
|||||||
type NavigationElementType,
|
type NavigationElementType,
|
||||||
} from '../context/ConstructorContext';
|
} from '../context/ConstructorContext';
|
||||||
import { useCanvasScale } from '../hooks/useCanvasScale';
|
import { useCanvasScale } from '../hooks/useCanvasScale';
|
||||||
import { useBackgroundDimensionSuggestion } from '../hooks/useBackgroundDimensionSuggestion';
|
|
||||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||||
import { CanvasDimensionSuggestion } from '../components/CanvasDimensionSuggestion';
|
|
||||||
|
|
||||||
// Constructor helpers (extracted utilities)
|
// Constructor helpers (extracted utilities)
|
||||||
import {
|
import {
|
||||||
@ -195,35 +193,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
backgroundVideoEndTime,
|
backgroundVideoEndTime,
|
||||||
} = usePageBackground();
|
} = usePageBackground();
|
||||||
|
|
||||||
// Background dimension auto-detection for canvas size suggestions
|
|
||||||
const handleDimensionSuggestionAccept = useCallback(
|
|
||||||
async (width: number, height: number) => {
|
|
||||||
if (!projectId) return;
|
|
||||||
try {
|
|
||||||
await axios.put(`/projects/${projectId}`, {
|
|
||||||
data: { design_width: width, design_height: height },
|
|
||||||
});
|
|
||||||
// Refetch to update the project data
|
|
||||||
await refetchData();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to update project dimensions', error instanceof Error ? error : { error });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectId, refetchData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
suggestion: dimensionSuggestion,
|
|
||||||
dismissSuggestion: dismissDimensionSuggestion,
|
|
||||||
acceptSuggestion: acceptDimensionSuggestion,
|
|
||||||
} = useBackgroundDimensionSuggestion({
|
|
||||||
mediaUrl: backgroundImageUrl || backgroundVideoUrl,
|
|
||||||
currentDesignWidth: project?.design_width ?? 1920,
|
|
||||||
currentDesignHeight: project?.design_height ?? 1080,
|
|
||||||
onSuggest: handleDimensionSuggestionAccept,
|
|
||||||
enabled: !isElementEditMode && !!project,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedMenuItem, setSelectedMenuItem] =
|
const [selectedMenuItem, setSelectedMenuItem] =
|
||||||
useState<EditorMenuItem>('none');
|
useState<EditorMenuItem>('none');
|
||||||
// Transition preview state managed by useTransitionPreview hook (below)
|
// Transition preview state managed by useTransitionPreview hook (below)
|
||||||
@ -389,6 +358,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
// Destructure stable callback reference to avoid infinite loops in useEffect deps
|
// Destructure stable callback reference to avoid infinite loops in useEffect deps
|
||||||
const pageSwitchToPage = pageSwitch.switchToPage;
|
const pageSwitchToPage = pageSwitch.switchToPage;
|
||||||
|
|
||||||
|
// Use shared background transition hook for direct navigation clearing and fade-in
|
||||||
|
// (No fade-out needed in constructor - transitions complete immediately)
|
||||||
|
// NOTE: Must be defined before switchToPage callback which uses resetFadeIn
|
||||||
|
const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
||||||
|
pageSwitch,
|
||||||
|
fadeIn: {
|
||||||
|
hasActiveTransition: Boolean(transitionPreview),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Helper to switch pages without flash
|
// Helper to switch pages without flash
|
||||||
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
||||||
// Also updates storage path state for editing/saving purposes
|
// Also updates storage path state for editing/saving purposes
|
||||||
@ -424,7 +403,12 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[pageSwitchToPage, updateBackgroundFromPage, applyPageSelection, resetFadeIn],
|
[
|
||||||
|
pageSwitchToPage,
|
||||||
|
updateBackgroundFromPage,
|
||||||
|
applyPageSelection,
|
||||||
|
resetFadeIn,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
|
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
|
||||||
@ -481,15 +465,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use shared background transition hook for direct navigation clearing and fade-in
|
|
||||||
// (No fade-out needed in constructor - transitions complete immediately)
|
|
||||||
const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
|
||||||
pageSwitch,
|
|
||||||
fadeIn: {
|
|
||||||
hasActiveTransition: Boolean(transitionPreview),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconPreloadTargets = useMemo(() => {
|
const iconPreloadTargets = useMemo(() => {
|
||||||
const preloadableTypes: CanvasElementType[] = [
|
const preloadableTypes: CanvasElementType[] = [
|
||||||
'navigation_next',
|
'navigation_next',
|
||||||
@ -1479,7 +1454,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={`z-20 overflow-clip ${hasFullWidthCarousel ? 'bg-transparent' : 'bg-black'}`}
|
className={`z-20 overflow-clip ${hasFullWidthCarousel ? 'bg-transparent' : 'bg-black'}`}
|
||||||
style={{ ...canvasCssVars, ...letterboxStyles, ...canvasBackgroundStyle }}
|
style={{
|
||||||
|
...canvasCssVars,
|
||||||
|
...letterboxStyles,
|
||||||
|
...canvasBackgroundStyle,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<BackdropPortalProvider>
|
<BackdropPortalProvider>
|
||||||
<CanvasBackground
|
<CanvasBackground
|
||||||
@ -1643,18 +1622,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Canvas Dimension Suggestion */}
|
|
||||||
{dimensionSuggestion && (
|
|
||||||
<CanvasDimensionSuggestion
|
|
||||||
suggestedWidth={dimensionSuggestion.width}
|
|
||||||
suggestedHeight={dimensionSuggestion.height}
|
|
||||||
currentWidth={project?.design_width ?? 1920}
|
|
||||||
currentHeight={project?.design_height ?? 1080}
|
|
||||||
onAccept={acceptDimensionSuggestion}
|
|
||||||
onDismiss={dismissDimensionSuggestion}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.menu-action-btn {
|
.menu-action-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -89,7 +89,11 @@ const Dashboard = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles) as {
|
const { rolesWidgets, loading } = useAppSelector((state) => state.roles) as {
|
||||||
rolesWidgets: Array<{ id?: string; widget_id?: string; [key: string]: unknown }>;
|
rolesWidgets: Array<{
|
||||||
|
id?: string;
|
||||||
|
widget_id?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -122,8 +122,10 @@ const EditProjectsPage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof project === 'object' && project !== null) {
|
if (typeof project === 'object' && project !== null) {
|
||||||
const projectData = project as unknown as Record<string, unknown>;
|
const projectData = project as unknown as Record<string, unknown>;
|
||||||
const width = Number(projectData.design_width) || CANVAS_CONFIG.defaults.width;
|
const width =
|
||||||
const height = Number(projectData.design_height) || CANVAS_CONFIG.defaults.height;
|
Number(projectData.design_width) || CANVAS_CONFIG.defaults.width;
|
||||||
|
const height =
|
||||||
|
Number(projectData.design_height) || CANVAS_CONFIG.defaults.height;
|
||||||
|
|
||||||
// Check if dimensions match a preset
|
// Check if dimensions match a preset
|
||||||
const matchesPreset = CANVAS_CONFIG.presets.some(
|
const matchesPreset = CANVAS_CONFIG.presets.some(
|
||||||
@ -409,7 +411,8 @@ const EditProjectsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<p className='text-xs text-gray-500 mt-1'>
|
<p className='text-xs text-gray-500 mt-1'>
|
||||||
Set to match your background image/video resolution for best
|
Set to match your background image/video resolution for best
|
||||||
quality. UI elements scale proportionally on different screens.
|
quality. UI elements scale proportionally on different
|
||||||
|
screens.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|||||||
@ -418,7 +418,8 @@ self.addEventListener('message', (event) => {
|
|||||||
if (payload?.storageKey) {
|
if (payload?.storageKey) {
|
||||||
// Extract storage path from presigned URL (or use storageKey directly)
|
// Extract storage path from presigned URL (or use storageKey directly)
|
||||||
const storagePath = payload.presignedUrl
|
const storagePath = payload.presignedUrl
|
||||||
? extractStoragePathFromUrl(payload.presignedUrl) || payload.storageKey
|
? extractStoragePathFromUrl(payload.presignedUrl) ||
|
||||||
|
payload.storageKey
|
||||||
: payload.storageKey;
|
: payload.storageKey;
|
||||||
storagePathToKeyMap.set(storagePath, payload.storageKey);
|
storagePathToKeyMap.set(storagePath, payload.storageKey);
|
||||||
console.log('[SW] Registered storage path for caching', {
|
console.log('[SW] Registered storage path for caching', {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user