diff --git a/web/css/queue_style.css b/web/css/queue_style.css index 241325c..5e00431 100644 --- a/web/css/queue_style.css +++ b/web/css/queue_style.css @@ -18,6 +18,17 @@ .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-thumb-wrap-placeholder{background:var(--surface-200,#212121);display:flex;align-items:center;justify-content:center;border-radius:4px;border:1px dashed var(--border-color,#4b4b4b);padding:6px;box-sizing:border-box;cursor:default} +.pqueue-thumb-wrap-placeholder--failed{border-color:var(--red-400,#f87171);background:rgba(248,113,113,0.08)} +.dark-theme .pqueue-thumb-wrap-placeholder--failed{background:rgba(248,113,113,0.12)} +.pqueue-thumb-wrap-placeholder--interrupted{border-color:var(--yellow-400,#facc15);background:rgba(250,204,21,0.08)} +.dark-theme .pqueue-thumb-wrap-placeholder--interrupted{background:rgba(250,204,21,0.12)} +.dark-theme .pqueue-thumb-wrap-placeholder{background:rgba(255,255,255,0.05)} +.pqueue-thumb-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.35rem;text-align:center;color:var(--text-color-secondary,#9ca3af);font-size:.76rem;line-height:1.2;padding:.25rem} +.pqueue-thumb-placeholder--failed{color:var(--red-400,#f87171)} +.pqueue-thumb-placeholder--interrupted{color:var(--yellow-500,#eab308)} +.pqueue-thumb-placeholder>i{font-size:1.25rem;opacity:.85} +.pqueue-thumb-placeholder-text{display:block;max-width:82px;word-break:break-word} .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} diff --git a/web/pqueue.js b/web/pqueue.js index a744503..ef0597b 100644 --- a/web/pqueue.js +++ b/web/pqueue.js @@ -228,41 +228,88 @@ UI.updateProgressBarsFromState(); }, + getThumbPlaceholderMeta(row) { + const status = String(row?.status ?? '').toLowerCase(); + if (['interrupted', 'cancelled', 'canceled', 'stopped'].includes(status)) { + return { text: 'Interrupted', iconClass: 'pi-stop-circle', variant: 'interrupted' }; + } + if (['error', 'failed', 'failure'].includes(status)) { + return { text: 'Failed', iconClass: 'pi-times-circle', variant: 'failed' }; + } + return { text: 'Preview unavailable', iconClass: 'pi-image-off', variant: 'empty' }; + }, + + createThumbPlaceholder(options = {}) { + const { text, iconClass, variant } = options ?? {}; + const classes = ['pqueue-thumb-placeholder']; + if (variant) classes.push(`pqueue-thumb-placeholder--${variant}`); + const placeholder = UI.el('div', { class: classes.join(' ') }); + const icon = UI.el('i', { class: `pi ${iconClass || 'pi-image-off'}` }); + const label = UI.el('span', { class: 'pqueue-thumb-placeholder-text', text: text || 'Preview unavailable' }); + placeholder.appendChild(icon); + placeholder.appendChild(label); + return placeholder; + }, + buildResultThumbs(row) { try { const container = UI.el('div', { class: 'flex gap-1 flex-wrap' }); + const placeholderMeta = UI.getThumbPlaceholderMeta(row); + const ensurePlaceholder = () => { + const wrapClasses = ['pqueue-thumb-wrap', 'pqueue-thumb-wrap-placeholder']; + if (placeholderMeta?.variant) wrapClasses.push(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`); + const wrap = UI.el('div', { class: wrapClasses.join(' ') }); + wrap.appendChild(UI.createThumbPlaceholder(placeholderMeta)); + container.appendChild(wrap); + }; + const attachFallbackHandler = (thumbWrap, img, images = []) => { + img.addEventListener('error', () => { + thumbWrap.classList.add('pqueue-thumb-wrap-placeholder'); + if (placeholderMeta?.variant) thumbWrap.classList.add(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`); + thumbWrap.replaceChildren(UI.createThumbPlaceholder(placeholderMeta)); + if (!images.length) thumbWrap.onclick = null; + }, { once: true }); + }; // Prefer stored DB thumbnail if available; bust cache with timestamp to avoid race if (row.id) { + const galleryImages = UI.extractImages(row); const url = new URL(`/api/pqueue/history/thumb/${row.id}`, window.location.origin); url.searchParams.set('_', String(Date.now())); const thumbWrap = UI.el('div', { class: 'pqueue-thumb-wrap' }); const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: `history-${row.id}` }); + attachFallbackHandler(thumbWrap, img, galleryImages); thumbWrap.appendChild(img); - if (UI.countImages(row) > 1) { - const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${UI.countImages(row)}` }); + const imgCount = UI.countImages(row); + if (imgCount > 1) { + const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${imgCount}` }); thumbWrap.appendChild(badge); } - thumbWrap.onclick = () => { - const images = UI.extractImages(row); - if (images.length) UI.openGallery(images, 0, row.prompt_id); - }; + if (galleryImages.length) { + thumbWrap.onclick = () => UI.openGallery(galleryImages, 0, row.prompt_id); + } else { + thumbWrap.onclick = null; + } container.appendChild(thumbWrap); - return container; } // Fallback to live preview route if DB thumb missing - const images = UI.extractImages(row); - if (!images.length) return container; - const first = images[0]; - const url = UI.buildPreviewUrl(first, row.prompt_id); - 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); + if (!container.children.length) { + const images = UI.extractImages(row); + if (images.length) { + const first = images[0]; + const url = UI.buildPreviewUrl(first, row.prompt_id); + const thumbWrap = UI.el('div', { class: 'pqueue-thumb-wrap' }); + const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: first.filename }); + attachFallbackHandler(thumbWrap, img, images); + 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, row.prompt_id); + container.appendChild(thumbWrap); + } } - thumbWrap.onclick = () => UI.openGallery(images, 0, row.prompt_id); - container.appendChild(thumbWrap); + if (!container.children.length) ensurePlaceholder(); return container; } catch(e) { return UI.el('span', {}); } },