// ── 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 = ``;
} else {
let html = '';
if (orphaned.length) {
html += `orphaned (not used by any card)
`;
html += orphaned.map(f => `
${mediaTile(f.url, 'border-t-danger/30')}
${esc(f.name)}
`).join('');
}
if (linked.length) {
if (orphaned.length) html += `linked to cards
`;
html += linked.map(img => `
${mediaTile(img.url, 'border-t-border')}
`).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(/\.[^.]+$/, '');
close();
window._openModal({ id: null, title: name, body: ``, color: '#38bdf8', bucket_id: null });
});
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);