// ===============================
// 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, name, type, cartella) {
// Se PATH_FULL è attivo, il backend ha già fornito un URL completo
if (window.PATH_FULL) {
return path;
}
// Comportamento normale
return (typeof toAbsoluteUrl === 'function')
? toAbsoluteUrl(path, name, type, cartella)
: path;
}
function mediaUrlsFromPhoto(photo) {
let original, preview;
if (window.PATH_FULL) {
// Percorsi completi già pronti dal backend
original = photo?.path;
preview = photo?.thub2 || photo?.thub1 || photo?.path;
} else {
// Costruzione URL come prima
original = absUrl(photo?.path, photo?.user, "original", photo?.cartella);
preview = absUrl(
photo?.thub2 || photo?.thub1 || photo?.path,
photo?.user,
"thumbs",
photo?.cartella
);
}
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(); });