378 lines
13 KiB
JavaScript
378 lines
13 KiB
JavaScript
// ===============================
|
||
// 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 = `
|
||
<strong>Impossibile riprodurre questo video nel browser.</strong>
|
||
${code === 4 ? 'Formato/codec non supportato (es. HEVC/H.265 su Chrome/Edge).' : 'Errore durante il caricamento.'}
|
||
<br><br>
|
||
Suggerimenti:
|
||
<ul style="margin:6px 0 0 18px">
|
||
<li><a href="${srcOriginal}" target="_blank" rel="noopener" style="color:#fff;text-decoration:underline">Apri il file in una nuova scheda</a></li>
|
||
<li>Prova Safari (supporta HEVC) oppure converti in MP4 (H.264 + AAC)</li>
|
||
</ul>
|
||
`;
|
||
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(); });
|