From 17b1a4d8eab14dd7ccad135ce92b8a0cffbe60ee Mon Sep 17 00:00:00 2001 From: Simon Cordt Date: Wed, 24 Sep 2025 09:38:13 +0200 Subject: [PATCH] Refactor Persistent Queue UI for improved image handling and gallery view --- web/css/queue_style.css | 12 ++- web/pqueue.js | 159 +++++++++++++++++++++++++++++++--------- 2 files changed, 135 insertions(+), 36 deletions(-) diff --git a/web/css/queue_style.css b/web/css/queue_style.css index 77a5d06..1410b19 100644 --- a/web/css/queue_style.css +++ b/web/css/queue_style.css @@ -15,7 +15,17 @@ .pqueue-status-success{color:var(--green-500)} .pqueue-status-error{color:var(--red-500)} .pqueue-empty{opacity:.7;font-size:.9rem;padding:.25rem 0} -.pqueue-thumbs-grid{display:grid;grid-template-columns:repeat(2, 96px);gap:6px} +.pqueue-thumbs-grid{display:grid;grid-template-columns:repeat(1, 96px);gap:6px} .pqueue-thumb{width:96px;height:96px;object-fit:cover;border-radius:4px;cursor:pointer} +.pqueue-thumb-wrap{position:relative;width:96px;height:96px} +.pqueue-batch-badge{position:absolute;right:4px;bottom:4px;padding:2px 6px;border-radius:10px;background:rgba(0,0,0,0.7);color:#fff;font-size:.75rem;line-height:1} +.pqueue-gallery-overlay{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.65);z-index:100000} +.pqueue-gallery{position:relative;max-width:92vw;max-height:92vh} +.pqueue-gallery-img{max-width:92vw;max-height:92vh;border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,.6)} +.pqueue-gallery-counter{position:absolute;left:50%;transform:translateX(-50%);bottom:-28px;color:#fff;font-size:.85rem;opacity:.9} +.pqueue-gallery-close{position:absolute;top:-36px;right:0;background:transparent;border:none;color:#fff;font-size:24px;cursor:pointer} +.pqueue-gallery-nav{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.5);border:none;color:#fff;font-size:24px;cursor:pointer;width:36px;height:36px;border-radius:18px} +.pqueue-gallery-prev{left:-44px} +.pqueue-gallery-next{right:-44px} .pqueue-progress-wrap{width:100%;height:8px;background:#1f2a1f;border-radius:4px;overflow:hidden;margin-top:6px} .pqueue-progress-bar{height:100%;width:0;background:#22c55e;transition:width .3s ease} \ No newline at end of file diff --git a/web/pqueue.js b/web/pqueue.js index 9cc6bcc..12ee670 100644 --- a/web/pqueue.js +++ b/web/pqueue.js @@ -232,37 +232,54 @@ buildResultThumbs(row) { try { - const wrap = UI.el('div', { class: 'flex gap-1 flex-wrap' }); - let outputs = row.outputs ?? {}; - if (typeof outputs === 'string') { - try { outputs = JSON.parse(outputs); } catch(e) { outputs = {}; } - } - const images = []; - Object.values(outputs).forEach(v => { - if (v && typeof v === 'object') { - const imgs = v.images ?? v.ui?.images ?? []; - if (Array.isArray(imgs)) imgs.forEach(i => images.push(i)); - } else if (Array.isArray(v)) { - v.forEach(i => { if (i?.filename || i?.name) images.push(i); }); - } - }); - images.slice(0, 4).forEach(i => { - const filename = i.filename ?? i.name ?? ''; - const type = i.type ?? 'output'; - const subfolder = i.subfolder ?? ''; - const url = new URL('/view', window.location.origin); - url.searchParams.set('filename', filename); - url.searchParams.set('type', type); - if (subfolder) url.searchParams.set('subfolder', subfolder); - url.searchParams.set('preview', 'webp;50'); - const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: filename }); - img.onclick = () => UI.openGallery(filename, type, subfolder); - wrap.appendChild(img); - }); - return wrap; + const container = UI.el('div', { class: 'flex gap-1 flex-wrap' }); + const images = UI.extractImages(row); + if (!images.length) return container; + const first = images[0]; + const url = new URL('/view', window.location.origin); + url.searchParams.set('filename', first.filename); + url.searchParams.set('type', first.type); + if (first.subfolder) url.searchParams.set('subfolder', first.subfolder); + url.searchParams.set('preview', 'webp;50'); + const thumbWrap = UI.el('div', { class: 'pqueue-thumb-wrap' }); + const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: first.filename }); + thumbWrap.appendChild(img); + if (images.length > 1) { + const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${images.length}` }); + thumbWrap.appendChild(badge); + } + thumbWrap.onclick = () => UI.openGallery(images, 0); + container.appendChild(thumbWrap); + return container; } catch(e) { return UI.el('span', {}); } }, + // Collect normalized image descriptors for a row + extractImages(row) { + try { + let outputs = row.outputs ?? {}; + if (typeof outputs === 'string') { + try { outputs = JSON.parse(outputs); } catch(e) { outputs = {}; } + } + const images = []; + Object.values(outputs).forEach(v => { + if (v && typeof v === 'object') { + const imgs = v.images ?? v.ui?.images ?? []; + if (Array.isArray(imgs)) imgs.forEach(i => { + const filename = i.filename ?? i.name; + if (filename) images.push({ filename, type: i.type ?? 'output', subfolder: i.subfolder ?? '' }); + }); + } else if (Array.isArray(v)) { + v.forEach(i => { + const filename = i?.filename ?? i?.name; + if (filename) images.push({ filename, type: i.type ?? 'output', subfolder: i.subfolder ?? '' }); + }); + } + }); + return images; + } catch(e) { return []; } + }, + buildDuration(row) { try { const dur = Number(row.duration_seconds); @@ -280,13 +297,85 @@ } catch(e) { return ''; } }, - openGallery(filename, type, subfolder) { - const url = new URL('/view', window.location.origin); - url.searchParams.set('filename', filename); - url.searchParams.set('type', type); - if (subfolder) url.searchParams.set('subfolder', subfolder); - window.open(url.href, '_blank'); - }, + // Lightbox-style gallery for image batches + openGallery(images, startIndex = 0) { + try { + UI.ensureGalleryElements(); + UI._galleryState.images = images || []; + UI._galleryState.index = Math.max(0, Math.min(startIndex, UI._galleryState.images.length - 1)); + UI._galleryOverlay.style.display = 'flex'; + UI.showGalleryImage(UI._galleryState.index); + UI._bindGalleryKeys(true); + } catch (e) { /* noop */ } + }, + + closeGallery() { + if (UI._galleryOverlay) UI._galleryOverlay.style.display = 'none'; + UI._bindGalleryKeys(false); + }, + + showGalleryImage(index) { + const imgs = UI._galleryState.images || []; + if (!imgs.length) return; + UI._galleryState.index = (index + imgs.length) % imgs.length; + const imgDesc = imgs[UI._galleryState.index]; + const url = new URL('/view', window.location.origin); + url.searchParams.set('filename', imgDesc.filename); + url.searchParams.set('type', imgDesc.type); + if (imgDesc.subfolder) url.searchParams.set('subfolder', imgDesc.subfolder); + UI._galleryImg.src = url.href; + UI._galleryCounter.textContent = `${UI._galleryState.index + 1} / ${imgs.length}`; + }, + + nextImage() { UI.showGalleryImage(UI._galleryState.index + 1); }, + prevImage() { UI.showGalleryImage(UI._galleryState.index - 1); }, + + _bindGalleryKeys(enabled) { + const handler = (e) => { + if (UI._galleryOverlay?.style.display !== 'flex') return; + if (e.key === 'Escape') UI.closeGallery(); + else if (e.key === 'ArrowRight') UI.nextImage(); + else if (e.key === 'ArrowLeft') UI.prevImage(); + }; + if (enabled) { + UI._galleryKeyHandler = handler; + document.addEventListener('keydown', handler); + } else if (UI._galleryKeyHandler) { + document.removeEventListener('keydown', UI._galleryKeyHandler); + UI._galleryKeyHandler = null; + } + }, + + ensureGalleryElements() { + if (UI._galleryOverlay) return; + UI._galleryState = { images: [], index: 0 }; + const overlay = UI.el('div', { class: 'pqueue-gallery-overlay' }); + const box = UI.el('div', { class: 'pqueue-gallery' }); + const closeBtn = UI.el('button', { class: 'pqueue-gallery-close', title: 'Close' }); + closeBtn.textContent = '×'; + const img = UI.el('img', { class: 'pqueue-gallery-img' }); + const counter = UI.el('div', { class: 'pqueue-gallery-counter' }); + const prev = UI.el('button', { class: 'pqueue-gallery-nav pqueue-gallery-prev', title: 'Previous' }); + prev.textContent = '‹'; + const next = UI.el('button', { class: 'pqueue-gallery-nav pqueue-gallery-next', title: 'Next' }); + next.textContent = '›'; + + closeBtn.onclick = () => UI.closeGallery(); + prev.onclick = () => UI.prevImage(); + next.onclick = () => UI.nextImage(); + overlay.onclick = (e) => { if (e.target === overlay) UI.closeGallery(); }; + + box.appendChild(closeBtn); + box.appendChild(img); + box.appendChild(counter); + box.appendChild(prev); + box.appendChild(next); + overlay.appendChild(box); + document.body.appendChild(overlay); + UI._galleryOverlay = overlay; + UI._galleryImg = img; + UI._galleryCounter = counter; + }, ensureStyles() { if (document.getElementById('pqueue-styles')) return;