|
import { Box, IconButton, Tooltip, CircularProgress } from "@mui/material"; |
|
import { LAYOUTS } from "./config"; |
|
import { groupSegmentsIntoLayouts } from "./utils"; |
|
import { useEffect, useRef, useState, useCallback } from "react"; |
|
import { Panel } from "./Panel"; |
|
import { StoryChoices } from "../components/StoryChoices"; |
|
import PhotoCameraIcon from "@mui/icons-material/PhotoCamera"; |
|
import { useGame } from "../contexts/GameContext"; |
|
import { useSoundEffect } from "../hooks/useSoundEffect"; |
|
|
|
|
|
function LoadingPage() { |
|
return ( |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
justifyContent: "center", |
|
alignItems: "center", |
|
height: "100%", |
|
aspectRatio: "0.7", |
|
flexShrink: 0, |
|
overflow: "hidden", |
|
}} |
|
> |
|
<CircularProgress |
|
size={60} |
|
sx={{ |
|
color: "white", |
|
opacity: 0.1, |
|
}} |
|
/> |
|
</Box> |
|
); |
|
} |
|
|
|
|
|
function ComicPage({ layout, layoutIndex, isLastPage, preloadedImages }) { |
|
const { |
|
handlePageLoaded, |
|
isLoading, |
|
isNarratorSpeaking, |
|
stopNarration, |
|
playNarration, |
|
heroName, |
|
} = useGame(); |
|
const [loadedImages, setLoadedImages] = useState(new Set()); |
|
const pageLoadedRef = useRef(false); |
|
const loadingTimeoutRef = useRef(null); |
|
const totalImages = layout.segments.reduce((total, segment) => { |
|
return total + (segment.images?.length || 0); |
|
}, 0); |
|
|
|
|
|
const [selectedTextPanelIndex] = useState(() => { |
|
const acceptingPanels = LAYOUTS[layout.type].panels |
|
.slice(0, totalImages) |
|
.map((panel, index) => ({ panel, index })) |
|
.filter(({ panel }) => panel.acceptText); |
|
|
|
if (acceptingPanels.length === 0) { |
|
|
|
return 0; |
|
} |
|
|
|
const randomIndex = Math.floor(Math.random() * acceptingPanels.length); |
|
return acceptingPanels[randomIndex].index; |
|
}); |
|
|
|
|
|
const playWritingSound = useSoundEffect({ |
|
basePath: "/sounds/drawing-", |
|
numSounds: 5, |
|
volume: 0.3, |
|
}); |
|
|
|
const handleImageLoad = useCallback((imageId) => { |
|
setLoadedImages((prev) => { |
|
|
|
if (prev.has(imageId)) { |
|
return prev; |
|
} |
|
|
|
const newSet = new Set(prev); |
|
newSet.add(imageId); |
|
return newSet; |
|
}); |
|
}, []); |
|
|
|
useEffect(() => { |
|
|
|
if (pageLoadedRef.current) return; |
|
|
|
|
|
if (loadingTimeoutRef.current) { |
|
clearTimeout(loadingTimeoutRef.current); |
|
} |
|
|
|
|
|
const expectedImageIds = Array.from( |
|
{ length: totalImages }, |
|
(_, i) => `page-${layoutIndex}-image-${i}` |
|
); |
|
|
|
|
|
const allImagesLoaded = expectedImageIds.every((id) => |
|
loadedImages.has(id) |
|
); |
|
|
|
if (allImagesLoaded && totalImages > 0) { |
|
|
|
loadingTimeoutRef.current = setTimeout(() => { |
|
if (!pageLoadedRef.current) { |
|
console.log(`Page ${layoutIndex} entièrement chargée`); |
|
pageLoadedRef.current = true; |
|
handlePageLoaded(layoutIndex); |
|
playWritingSound(); |
|
} |
|
}, 100); |
|
} |
|
|
|
return () => { |
|
if (loadingTimeoutRef.current) { |
|
clearTimeout(loadingTimeoutRef.current); |
|
} |
|
}; |
|
}, [ |
|
loadedImages, |
|
totalImages, |
|
layoutIndex, |
|
handlePageLoaded, |
|
playWritingSound, |
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
flexDirection: "row", |
|
gap: 2, |
|
height: "100%", |
|
position: "relative", |
|
}} |
|
> |
|
<Box |
|
data-comic-page |
|
sx={{ |
|
display: "grid", |
|
gridTemplateColumns: `repeat(${LAYOUTS[layout.type].gridCols}, 1fr)`, |
|
gridTemplateRows: `repeat(${LAYOUTS[layout.type].gridRows}, 1fr)`, |
|
gap: 2, |
|
height: "100%", |
|
aspectRatio: "0.7", |
|
backgroundColor: "white", |
|
boxShadow: "0 0 10px rgba(0,0,0,0.1)", |
|
borderRadius: "4px", |
|
p: 2, |
|
pb: 4, |
|
flexShrink: 0, |
|
position: "relative", |
|
}} |
|
> |
|
{LAYOUTS[layout.type].panels |
|
.slice(0, totalImages) |
|
.map((panel, panelIndex) => { |
|
// Trouver le segment qui contient l'image pour ce panel |
|
let currentImageIndex = 0; |
|
let targetSegment = null; |
|
let targetImageIndex = 0; |
|
|
|
for (const segment of layout.segments) { |
|
const segmentImageCount = segment.images?.length || 0; |
|
if (currentImageIndex + segmentImageCount > panelIndex) { |
|
targetSegment = segment; |
|
targetImageIndex = panelIndex - currentImageIndex; |
|
// console.log("Found image for panel:", { |
|
// panelIndex, |
|
// targetImageIndex, |
|
// hasImages: !!segment.images, |
|
// imageCount: segment.images?.length, |
|
// imageDataSample: |
|
// segment.images?.[targetImageIndex]?.slice(0, 50) + "...", |
|
// }); |
|
break; |
|
} |
|
currentImageIndex += segmentImageCount; |
|
} |
|
|
|
return ( |
|
<Panel |
|
key={panelIndex} |
|
panel={panel} |
|
segment={targetSegment} |
|
panelIndex={targetImageIndex} |
|
totalImagesInPage={totalImages} |
|
onImageLoad={() => |
|
handleImageLoad(`page-${layoutIndex}-image-${panelIndex}`) |
|
} |
|
imageId={`page-${layoutIndex}-image-${panelIndex}`} |
|
showText={panelIndex === selectedTextPanelIndex} |
|
/> |
|
); |
|
})} |
|
<Box |
|
sx={{ |
|
position: "absolute", |
|
bottom: 8, |
|
left: 0, |
|
right: 0, |
|
textAlign: "center", |
|
color: "black", |
|
fontSize: "0.875rem", |
|
fontWeight: 500, |
|
}} |
|
> |
|
{layoutIndex + 1} |
|
</Box> |
|
</Box> |
|
</Box> |
|
); |
|
} |
|
|
|
|
|
const imageCache = new Map(); |
|
|
|
|
|
export function ComicLayout() { |
|
const { |
|
segments, |
|
isLoading, |
|
playNarration, |
|
stopNarration, |
|
isNarratorSpeaking, |
|
} = useGame(); |
|
const scrollContainerRef = useRef(null); |
|
const [preloadedImages, setPreloadedImages] = useState(new Map()); |
|
const preloadingRef = useRef(false); |
|
|
|
const loadImage = async (imageData, imageId) => { |
|
|
|
if (!imageData || typeof imageData !== "string" || imageData.length === 0) { |
|
console.warn( |
|
`Image invalide pour ${imageId}: données manquantes ou invalides` |
|
); |
|
return Promise.reject(new Error("Données d'image invalides")); |
|
} |
|
|
|
|
|
if (imageCache.has(imageId)) { |
|
return imageCache.get(imageId); |
|
} |
|
|
|
|
|
if (preloadingRef.current.has(imageId)) { |
|
return; |
|
} |
|
|
|
preloadingRef.current.add(imageId); |
|
|
|
try { |
|
const img = new Image(); |
|
const imagePromise = new Promise((resolve, reject) => { |
|
img.onload = () => { |
|
imageCache.set(imageId, imageData); |
|
preloadingRef.current.delete(imageId); |
|
resolve(imageData); |
|
}; |
|
img.onerror = (error) => { |
|
preloadingRef.current.delete(imageId); |
|
console.warn(`Échec du chargement de l'image ${imageId}`, error); |
|
reject(new Error(`Échec du chargement de l'image ${imageId}`)); |
|
}; |
|
}); |
|
|
|
img.src = `data:image/jpeg;base64,${imageData}`; |
|
return await imagePromise; |
|
} catch (error) { |
|
preloadingRef.current.delete(imageId); |
|
throw error; |
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
if (!segments?.length) return; |
|
|
|
preloadingRef.current = new Set(); |
|
const newPreloadedImages = new Map(); |
|
|
|
const loadAllImages = async () => { |
|
for ( |
|
let segmentIndex = 0; |
|
segmentIndex < segments.length; |
|
segmentIndex++ |
|
) { |
|
const segment = segments[segmentIndex]; |
|
|
|
|
|
if (!segment?.images?.length) { |
|
console.warn(`Segment ${segmentIndex} invalide ou sans images`); |
|
continue; |
|
} |
|
|
|
for ( |
|
let imageIndex = 0; |
|
imageIndex < segment.images.length; |
|
imageIndex++ |
|
) { |
|
const imageData = segment.images[imageIndex]; |
|
const imageId = `segment-${segmentIndex}-image-${imageIndex}`; |
|
|
|
try { |
|
if (!imageData) { |
|
console.warn(`Image manquante: ${imageId}`); |
|
newPreloadedImages.set(imageId, false); |
|
continue; |
|
} |
|
|
|
await loadImage(imageData, imageId); |
|
newPreloadedImages.set(imageId, true); |
|
} catch (error) { |
|
console.warn( |
|
`Erreur lors du chargement de ${imageId}:`, |
|
error.message |
|
); |
|
newPreloadedImages.set(imageId, false); |
|
} |
|
} |
|
} |
|
setPreloadedImages(new Map(newPreloadedImages)); |
|
}; |
|
|
|
loadAllImages(); |
|
|
|
return () => { |
|
preloadingRef.current = new Set(); |
|
}; |
|
}, [segments]); |
|
|
|
|
|
useEffect(() => { |
|
const loadedSegments = segments.filter((segment) => !segment.isLoading); |
|
const lastSegment = loadedSegments[loadedSegments.length - 1]; |
|
|
|
if (scrollContainerRef.current && lastSegment) { |
|
|
|
scrollContainerRef.current.scrollTo({ |
|
left: scrollContainerRef.current.scrollWidth, |
|
behavior: "smooth", |
|
}); |
|
} |
|
}, [segments]); |
|
|
|
|
|
useEffect(() => { |
|
const container = scrollContainerRef.current; |
|
if (!container) return; |
|
|
|
const handleWheel = (e) => { |
|
const max = container.scrollWidth - container.offsetWidth; |
|
if ( |
|
container.scrollLeft + e.deltaX < 0 || |
|
container.scrollLeft + e.deltaX > max |
|
) { |
|
e.preventDefault(); |
|
container.scrollLeft = Math.max( |
|
0, |
|
Math.min(max, container.scrollLeft + e.deltaX) |
|
); |
|
} |
|
}; |
|
|
|
container.addEventListener("wheel", handleWheel, { passive: false }); |
|
return () => container.removeEventListener("wheel", handleWheel); |
|
}, []); |
|
|
|
const loadedSegments = segments.filter((segment) => segment.text); |
|
const layouts = groupSegmentsIntoLayouts(loadedSegments); |
|
|
|
return ( |
|
<Box |
|
ref={scrollContainerRef} |
|
data-comic-layout |
|
sx={{ |
|
display: "flex", |
|
flexDirection: "row", |
|
gap: 4, |
|
height: "100%", |
|
width: "100%", |
|
px: { |
|
xs: 2, // 4 en mobile |
|
sm: "calc(50% - 25vw)", // Valeur originale pour les écrans plus grands |
|
}, |
|
pt: 4, |
|
pb: 0, |
|
overflowX: "auto", |
|
overflowY: "hidden", |
|
"&::-webkit-scrollbar": { |
|
height: "0px", |
|
}, |
|
"&::-webkit-scrollbar-track": { |
|
backgroundColor: "grey.800", |
|
}, |
|
"&::-webkit-scrollbar-thumb": { |
|
backgroundColor: "grey.700", |
|
borderRadius: "4px", |
|
}, |
|
}} |
|
> |
|
{layouts.map((layout, layoutIndex) => ( |
|
<ComicPage |
|
key={layoutIndex} |
|
layout={layout} |
|
layoutIndex={layoutIndex} |
|
isLastPage={layoutIndex === layouts.length - 1} |
|
preloadedImages={preloadedImages} |
|
/> |
|
))} |
|
</Box> |
|
); |
|
} |
|
|