// =============================== // MODALE (FOTO + VIDEO) — avanzato con navigazione e preload // - Sostituisce il contenuto, non accumula // - Chiude la bottom-zone quando si apre // - Prev/Next (←/→ e click ai bordi), preload 3+3 // - Pulsante INFO (ℹ️) riportato dentro il modal con toggle affidabile // =============================== const modal = document.getElementById('modal'); const modalClose = document.getElementById('modalClose'); window.currentPhoto = null; // usato anche da infoPanel window.modalList = []; // lista corrente per navigazione window.modalIndex = 0; // indice corrente nella lista // Frecce visibili const modalPrev = document.getElementById('modalPrev'); const modalNext = document.getElementById('modalNext'); // =============================== // Stato/Helper Info Panel (toggle affidabile) // =============================== let infoOpen = false; // stato interno affidabile function getInfoPanel() { return document.getElementById('infoPanel'); } function isInfoOpen() { return infoOpen; } function openInfo(photo) { // Prova API esplicita, altrimenti fallback a toggle try { if (typeof window.openInfoPanel === 'function') { window.openInfoPanel(photo); } else if (typeof window.toggleInfoPanel === 'function') { window.toggleInfoPanel(photo); } } catch {} infoOpen = true; const panel = getInfoPanel(); panel?.classList.add('open'); panel?.setAttribute('aria-hidden', 'false'); panel?.setAttribute('data-open', '1'); document.getElementById('modalInfoBtn')?.classList.add('active'); } function closeInfo() { // Prova API esplicita, altrimenti fallback a toggle (senza argomento) try { if (typeof window.closeInfoPanel === 'function') { window.closeInfoPanel(); } else if (typeof window.toggleInfoPanel === 'function') { window.toggleInfoPanel(); } } catch {} infoOpen = false; const panel = getInfoPanel(); panel?.classList.remove('open'); panel?.setAttribute('aria-hidden', 'true'); panel?.setAttribute('data-open', '0'); document.getElementById('modalInfoBtn')?.classList.remove('active'); } function toggleInfo(photo) { if (isInfoOpen()) closeInfo(); else openInfo(photo); } // =============================== // Utility MIME / media // =============================== function isProbablyVideo(photo, srcOriginal) { const mime = String(photo?.mime_type || '').toLowerCase(); if (mime.startsWith('video/')) return true; return /\.(mp4|m4v|webm|mov|qt|avi|mkv)$/i.test(String(srcOriginal || '')); } function guessVideoMime(photo, srcOriginal) { let t = String(photo?.mime_type || '').toLowerCase(); if (t && t !== 'application/octet-stream') return t; const src = String(srcOriginal || ''); if (/\.(mp4|m4v)$/i.test(src)) return 'video/mp4'; if (/\.(webm)$/i.test(src)) return 'video/webm'; if (/\.(mov|qt)$/i.test(src)) return 'video/quicktime'; if (/\.(avi)$/i.test(src)) return 'video/x-msvideo'; if (/\.(mkv)$/i.test(src)) return 'video/x-matroska'; return ''; } function createVideoElement(srcOriginal, srcPreview, photo) { const video = document.createElement('video'); video.controls = true; video.playsInline = true; // iOS: evita fullscreen nativo video.setAttribute('webkit-playsinline', ''); // compat iOS storici video.preload = 'metadata'; video.poster = srcPreview || ''; video.style.maxWidth = '100%'; video.style.maxHeight = '100%'; video.style.objectFit = 'contain'; const source = document.createElement('source'); source.src = srcOriginal; const type = guessVideoMime(photo, srcOriginal); if (type) source.type = type; video.appendChild(source); video.addEventListener('loadedmetadata', () => { try { video.currentTime = 0.001; } catch (_) {} console.log('[video] loadedmetadata', { w: video.videoWidth, h: video.videoHeight, dur: video.duration }); }); video.addEventListener('error', () => { const code = video.error && video.error.code; console.warn('[video] error code:', code, 'type:', type, 'src:', srcOriginal); const msg = document.createElement('div'); msg.style.padding = '12px'; msg.style.color = '#fff'; msg.style.background = 'rgba(0,0,0,0.6)'; msg.style.borderRadius = '8px'; msg.innerHTML = ` Impossibile riprodurre questo video nel browser. ${code === 4 ? 'Formato/codec non supportato (es. HEVC/H.265 su Chrome/Edge).' : 'Errore durante il caricamento.'}

Suggerimenti: `; const container = document.getElementById('modalMediaContainer'); container && container.appendChild(msg); }); // Evita di far scattare la navigazione "ai bordi" video.addEventListener('click', (e) => e.stopPropagation()); return video; } function createImageElement(srcOriginal, srcPreview) { const img = document.createElement('img'); img.src = srcPreview || srcOriginal || ''; img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.objectFit = 'contain'; // Progressive loading: preview → fullres if (srcPreview && srcOriginal && srcPreview !== srcOriginal) { const full = new Image(); full.src = srcOriginal; full.onload = () => { img.src = srcOriginal; }; } return img; } // =============================== // Helpers per URL assoluti // =============================== function absUrl(path) { return (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(path) : path; } function mediaUrlsFromPhoto(photo) { const original = absUrl(photo?.path); const preview = absUrl(photo?.thub2 || photo?.thub1 || photo?.path); return { original, preview }; } // =============================== // PRELOAD ±N (solo immagini; per i video: poster/preview) // =============================== function preloadNeighbors(N = 3) { const list = window.modalList || []; const idx = window.modalIndex || 0; for (let offset = 1; offset <= N; offset++) { const iPrev = idx - offset; const iNext = idx + offset; [iPrev, iNext].forEach(i => { const p = list[i]; if (!p) return; const { original, preview } = mediaUrlsFromPhoto(p); const isVideo = String(p?.mime_type || '').toLowerCase().startsWith('video/'); const src = isVideo ? (preview || original) : original; if (!src) return; const img = new Image(); img.src = src; }); } } // =============================== // Core: imposta contenuto modal // =============================== function setModalContent(photo, srcOriginal, srcPreview) { const container = document.getElementById('modalMediaContainer'); container.innerHTML = ''; window.currentPhoto = photo; const isVideo = isProbablyVideo(photo, srcOriginal); console.log('[openModal]', { isVideo, mime: photo?.mime_type, srcOriginal, srcPreview }); if (isVideo) { const video = createVideoElement(srcOriginal, srcPreview, photo); container.appendChild(video); } else { const img = createImageElement(srcOriginal, srcPreview); container.appendChild(img); } // Pulsante INFO (ℹ️) dentro il modal — toggle vero const infoBtn = document.createElement('button'); infoBtn.id = 'modalInfoBtn'; infoBtn.className = 'modal-info-btn'; infoBtn.type = 'button'; infoBtn.setAttribute('aria-label', 'Dettagli'); infoBtn.textContent = 'ℹ️'; container.appendChild(infoBtn); infoBtn.addEventListener('click', (e) => { e.stopPropagation(); // non far scattare navigazione toggleInfo(window.currentPhoto); }); } // =============================== // API base: open/close modal (mantiene sostituzione contenuto) // =============================== function openModal(srcOriginal, srcPreview, photo) { // Chiudi sempre la strip prima di aprire window.closeBottomSheet?.(); setModalContent(photo, srcOriginal, srcPreview); modal.classList.add('open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function closeModal() { // Chiudi anche l'info se aperto if (isInfoOpen()) closeInfo(); const v = document.querySelector('#modal video'); if (v) { try { v.pause(); } catch (_) {} v.removeAttribute('src'); while (v.firstChild) v.removeChild(v.firstChild); try { v.load(); } catch (_) {} } const container = document.getElementById('modalMediaContainer'); if (container) container.innerHTML = ''; modal.classList.remove('open'); modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; // Nascondi frecce alla chiusura (così non "rimangono" visibili) try { modalPrev?.classList.add('hidden'); modalNext?.classList.add('hidden'); } catch {} } // X: stopPropagation + chiudi modalClose?.addEventListener('click', (e) => { e.stopPropagation(); closeModal(); }); // Backdrop: chiudi cliccando fuori modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // =============================== // Navigazione: lista + indice + prev/next + click ai bordi + tastiera // =============================== function openAt(i) { const list = window.modalList || []; if (!list[i]) return; window.modalIndex = i; const photo = list[i]; const { original, preview } = mediaUrlsFromPhoto(photo); // Se l'info è aperto, aggiorna i contenuti per la nuova foto if (isInfoOpen()) { openInfo(photo); } openModal(original, preview, photo); // sostituisce contenuto preloadNeighbors(3); updateArrows(); } window.openModalFromList = function(list, index) { window.modalList = Array.isArray(list) ? list : []; window.modalIndex = Math.max(0, Math.min(index || 0, window.modalList.length - 1)); openAt(window.modalIndex); }; function showPrev() { if (window.modalIndex > 0) openAt(window.modalIndex - 1); } function showNext() { if (window.modalIndex < (window.modalList.length - 1)) openAt(window.modalIndex + 1); } // Tastiera document.addEventListener('keydown', (e) => { if (!modal.classList.contains('open')) return; if (e.key === 'ArrowLeft') { e.preventDefault(); showPrev(); } if (e.key === 'ArrowRight') { e.preventDefault(); showNext(); } }); // Click ai bordi del modal: sinistra=prev, destra=next (ignora controlli) modal.addEventListener('click', (e) => { if (!modal.classList.contains('open')) return; // Ignora click sui controlli if (e.target.closest('.modal-info-btn, .modal-close, .modal-nav-btn')) return; if (e.target === modal) return; // già gestito per chiusura const rect = modal.getBoundingClientRect(); const x = e.clientX - rect.left; const side = x / rect.width; if (side < 0.25) showPrev(); else if (side > 0.75) showNext(); }); // Esporta API base (per compatibilità con codice esistente) window.openModal = openModal; window.closeModal = closeModal; // =============================== // FRECCE DI NAVIGAZIONE < > // =============================== function updateArrows() { if (!modalPrev || !modalNext) return; const len = (window.modalList || []).length; const i = window.modalIndex || 0; // Mostra frecce solo se ci sono almeno 2 elementi const show = len > 1; modalPrev.classList.toggle('hidden', !show); modalNext.classList.toggle('hidden', !show); // Disabilita ai bordi (no wrap) modalPrev.classList.toggle('disabled', i <= 0); modalNext.classList.toggle('disabled', i >= len - 1); } // Click sulle frecce: non propagare (evita conflitti col click sui bordi) modalPrev?.addEventListener('click', (e) => { e.stopPropagation(); showPrev(); updateArrows(); }); modalNext?.addEventListener('click', (e) => { e.stopPropagation(); showNext(); updateArrows(); });