photo_server_json_flutter_c.../public/js/modal.js
2026-02-26 11:48:04 +01:00

350 lines
No EOL
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ===============================
// 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) {
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(); });