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"; // Composant pour afficher le spinner de chargement function LoadingPage() { return ( ); } // Component for displaying a page of panels 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); // Sélectionner aléatoirement un panneau qui accepte le texte 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) { // Si aucun panneau n'accepte le texte, utiliser le premier panneau par défaut return 0; } // Sélectionner un panneau aléatoire parmi ceux qui acceptent le texte const randomIndex = Math.floor(Math.random() * acceptingPanels.length); return acceptingPanels[randomIndex].index; }); // Son d'écriture const playWritingSound = useSoundEffect({ basePath: "/sounds/drawing-", numSounds: 5, volume: 0.3, }); const handleImageLoad = useCallback((imageId) => { setLoadedImages((prev) => { // Si l'image est déjà chargée, ne rien faire if (prev.has(imageId)) { return prev; } const newSet = new Set(prev); newSet.add(imageId); return newSet; }); }, []); useEffect(() => { // Si la page a déjà été marquée comme chargée, ne rien faire if (pageLoadedRef.current) return; // Nettoyer le timeout précédent si existant if (loadingTimeoutRef.current) { clearTimeout(loadingTimeoutRef.current); } // Générer les IDs attendus pour cette page const expectedImageIds = Array.from( { length: totalImages }, (_, i) => `page-${layoutIndex}-image-${i}` ); // Vérifier si toutes les images de la page sont chargées const allImagesLoaded = expectedImageIds.every((id) => loadedImages.has(id) ); if (allImagesLoaded && totalImages > 0) { // Utiliser un timeout pour éviter les appels trop fréquents 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, ]); // console.log("ComicPage layout:", { // type: layout.type, // totalImages, // loadedImages: loadedImages.size, // segments: layout.segments, // isLastPage, // hasChoices: choices?.length > 0, // showScreenshot, // }); return ( {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 ( handleImageLoad(`page-${layoutIndex}-image-${panelIndex}`) } imageId={`page-${layoutIndex}-image-${panelIndex}`} showText={panelIndex === selectedTextPanelIndex} /> ); })} {layoutIndex + 1} ); } // Cache global pour stocker les images préchargées const imageCache = new Map(); // Main comic layout component 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) => { // Vérifier si l'image est valide 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")); } // Si l'image est déjà dans le cache, ne pas la recharger if (imageCache.has(imageId)) { return imageCache.get(imageId); } // Si l'image est déjà en cours de chargement, ne pas la recharger 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; } }; // Précharger les images pour tous les segments 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]; // Vérifier si le segment et ses images sont valides 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]); // Effect to scroll to the right when segments are loaded useEffect(() => { const loadedSegments = segments.filter((segment) => !segment.isLoading); const lastSegment = loadedSegments[loadedSegments.length - 1]; if (scrollContainerRef.current && lastSegment) { // Scroll to the right scrollContainerRef.current.scrollTo({ left: scrollContainerRef.current.scrollWidth, behavior: "smooth", }); } }, [segments]); // Prevent back/forward navigation on trackpad horizontal scroll 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 ( {layouts.map((layout, layoutIndex) => ( ))} ); }