// Полноэкранный просмотр фото и видео из вложений: листание, «Скачать». // Закрывается крестиком, Esc, смахиванием вниз или переходом назад. import { iconHtml } from './ui-icon.js'; let activeClose = null; function safeFileName(name, kind) { const clean = String(name || '').trim().replace(/[\\/:*?"<>|]+/g, '_'); if (clean) return clean; return kind === 'video' ? 'video.mp4' : 'image.jpg'; } // Arweave отдаёт файл с другого адреса: атрибут download там не работает, поэтому скачиваем через blob. async function downloadMedia(item) { const name = safeFileName(item.name, item.kind); try { const response = await fetch(item.url); if (!response.ok) throw new Error(String(response.status)); const blobUrl = URL.createObjectURL(await response.blob()); const link = document.createElement('a'); link.href = blobUrl; link.download = name; document.body.append(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 30000); } catch { window.open(item.url, '_blank', 'noopener'); } } /** * items: [{ url, kind: 'image' | 'video', name }], index – с какого начать. */ export function openMediaViewer(items = [], index = 0) { const list = items.filter((item) => item?.url && (item.kind === 'image' || item.kind === 'video')); if (!list.length) return; activeClose?.(); let current = Math.max(0, Math.min(list.length - 1, index)); const root = document.getElementById('modal-root') || document.body; const overlay = document.createElement('div'); overlay.className = 'media-viewer'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', 'Просмотр'); // Сверху – только «закрыть»; счётчик и «Скачать» – сразу под фото, по центру. overlay.innerHTML = `
`; const stage = overlay.querySelector('.media-viewer__stage'); const frame = overlay.querySelector('.media-viewer__frame'); const counter = overlay.querySelector('.media-viewer__counter'); const prevBtn = overlay.querySelector('[data-action="prev"]'); const nextBtn = overlay.querySelector('[data-action="next"]'); const render = () => { stage.querySelectorAll('video').forEach((video) => video.pause()); const item = list[current]; let media; if (item.kind === 'video') { media = document.createElement('video'); media.controls = true; media.autoplay = true; media.playsInline = true; } else { media = document.createElement('img'); media.alt = item.name || ''; media.decoding = 'async'; } media.className = 'media-viewer__media'; media.src = item.url; frame.replaceChildren(media); counter.textContent = list.length > 1 ? `${current + 1}/${list.length}` : ''; counter.hidden = list.length <= 1; prevBtn.hidden = current <= 0; nextBtn.hidden = current >= list.length - 1; }; const go = (delta) => { const next = current + delta; if (next < 0 || next >= list.length) return; current = next; render(); }; const onKey = (event) => { if (event.key === 'Escape') close(); else if (event.key === 'ArrowLeft') go(-1); else if (event.key === 'ArrowRight') go(1); }; const onPopState = () => close(); const close = () => { if (activeClose !== close) return; activeClose = null; document.removeEventListener('keydown', onKey); window.removeEventListener('popstate', onPopState); stage.querySelectorAll('video').forEach((video) => video.pause()); overlay.remove(); }; let swiped = false; overlay.addEventListener('click', (event) => { // Клик, который браузер присылает после смахивания, не должен прятать панель или закрывать просмотр. if (swiped) { swiped = false; return; } const action = event.target.closest('[data-action]')?.dataset.action; if (action === 'close') close(); else if (action === 'prev') go(-1); else if (action === 'next') go(1); else if (action === 'download') void downloadMedia(list[current]); else if (event.target.closest('.media-viewer__media')?.tagName === 'IMG') overlay.classList.toggle('is-bare'); else if (event.target === stage || event.target === frame || event.target === overlay) close(); }); // Смахивание: вбок – листать, вниз – закрыть. let startX = 0; let startY = 0; let tracking = false; stage.addEventListener('pointerdown', (event) => { if (event.target.tagName === 'VIDEO') return; tracking = true; startX = event.clientX; startY = event.clientY; }); stage.addEventListener('pointermove', (event) => { if (!tracking) return; const dy = event.clientY - startY; if (dy > 0 && Math.abs(dy) > Math.abs(event.clientX - startX)) { stage.style.transform = `translateY(${dy}px)`; overlay.style.setProperty('--viewer-dim', String(Math.max(0.4, 1 - dy / 400))); } }); const endSwipe = (event) => { if (!tracking) return; tracking = false; const dx = event.clientX - startX; const dy = event.clientY - startY; swiped = Math.hypot(dx, dy) > 10; stage.style.transform = ''; overlay.style.removeProperty('--viewer-dim'); if (dy > 90 && dy > Math.abs(dx)) close(); else if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) go(dx < 0 ? 1 : -1); }; stage.addEventListener('pointerup', endSwipe); stage.addEventListener('pointercancel', endSwipe); document.addEventListener('keydown', onKey); window.addEventListener('popstate', onPopState); activeClose = close; render(); root.append(overlay); overlay.querySelector('[data-action="close"]')?.focus({ preventScroll: true }); }