// ── Image gallery ── import { $, esc, api, getCsrf } from './util.js'; const AUDIO_EXT = /\.(mp3|ogg|wav|m4a)$/i; function isAudio(url) { return AUDIO_EXT.test(url); } function mediaTile(url, extraClass = '') { if (isAudio(url)) { return `
`; } return `
`; } import { state } from './state.js'; import { showDialog } from './dialog.js'; function getReferencedUrls() { const urls = new Set(); for (const card of state.cards) { const body = card.body || ''; let m; const mdRe = /!\[[^\]]*\]\(([^)]+)\)/g; while ((m = mdRe.exec(body)) !== null) urls.add(m[1].split(/\s/)[0]); const htmlRe = /]+src=["']([^"']+)["'][^>]*>/gi; while ((m = htmlRe.exec(body)) !== null) urls.add(m[1]); } return urls; } function findCardForUrl(url) { for (const card of state.cards) { const body = card.body || ''; if (body.includes(url)) return card; } return null; } export async function openGallery() { const el = $('gallery'); if (!el) return; const files = await api('api/uploads'); const referenced = getReferencedUrls(); const grid = $('gallery-grid'); const count = $('gallery-count'); const linked = []; const orphaned = []; for (const f of files) { const card = findCardForUrl(f.url); if (card) { linked.push({ ...f, cardId: card.id, cardTitle: card.title, cardColor: card.color }); } else { orphaned.push(f); } } const total = linked.length + orphaned.length; if (count) count.textContent = `${total} image${total !== 1 ? 's' : ''}${orphaned.length ? ` · ${orphaned.length} orphaned` : ''}`; if (!total) { grid.innerHTML = `

no images

`; } else { let html = ''; if (orphaned.length) { html += `
orphaned (not used by any card)
`; html += orphaned.map(f => ` `).join(''); } if (linked.length) { if (orphaned.length) html += `
linked to cards
`; html += linked.map(img => ` `).join(''); } grid.innerHTML = html; // Linked image clicks → open card grid.querySelectorAll('[data-card-id]').forEach(item => { item.addEventListener('click', () => { window._openDetail(item.dataset.cardId, { fromGallery: true, galleryOrigin: item.dataset.galleryOrigin || null, }); }); }); // Orphan: click to enlarge grid.querySelectorAll('[data-orphan]').forEach(item => { item.addEventListener('click', e => { if (e.target.closest('button')) return; const name = item.dataset.orphan; const f = orphaned.find(o => o.name === name); if (f && !isAudio(f.url)) openOrphanLightbox(f); }); }); } el.classList.add('open'); } function openOrphanLightbox(file) { let overlay = document.querySelector('.orphan-lightbox'); if (overlay) overlay.remove(); overlay = document.createElement('div'); overlay.className = 'orphan-lightbox'; overlay.style.cssText = 'position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:rgba(2,6,23,0.92);'; overlay.innerHTML = `
`; const close = () => overlay.remove(); overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); overlay.querySelector('.orphan-lb-close').addEventListener('click', close); overlay.querySelector('.orphan-lb-create').addEventListener('click', async () => { const name = file.url.split('/').pop().replace(/\.[^.]+$/, ''); await api('api/cards', { method: 'POST', body: { title: name, body: `![${name}](${file.url})`, color: '#38bdf8', position: -1 } }); await window._reloadAll(); close(); openGallery(); }); overlay.querySelector('.orphan-lb-delete').addEventListener('click', async () => { const ok = await showDialog({ title: 'delete_image', message: `"${file.name}" loeschen?`, danger: true, okLabel: 'delete' }); if (!ok) return; await fetch(`api/uploads/${file.name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } }); close(); openGallery(); }); document.body.appendChild(overlay); } export function closeGallery() { $('gallery')?.classList.remove('open'); } $('gallery-close')?.addEventListener('click', closeGallery); $('gallery')?.addEventListener('click', e => { if (e.target === $('gallery')) closeGallery(); }); $('gallery-btn')?.addEventListener('click', openGallery);