diff --git a/web/pqueue.js b/web/pqueue.js
index 4ee0c76..70634b1 100644
--- a/web/pqueue.js
+++ b/web/pqueue.js
@@ -24,282 +24,400 @@
selectedPending: new Set(),
};
- function el(tag, attrs={}, children=[]) {
- const e = document.createElement(tag);
- Object.entries(attrs).forEach(([k,v]) => {
- if (k === 'class') e.className = v; else if (k === 'text') e.textContent = v; else e.setAttribute(k, v);
- });
- children.forEach(c => e.appendChild(c));
- return e;
- }
+ const Progress = {
+ computeAggregateProgress(nodes) {
+ let totalMax = 0;
+ let totalVal = 0;
+ Object.values(nodes ?? {}).forEach((st) => {
+ const mv = Math.max(1, Number(st?.max ?? 1));
+ const vv = Math.max(0, Math.min(Number(st?.value ?? 0), mv));
+ totalMax += mv;
+ totalVal += vv;
+ });
+ return totalMax > 0 ? Math.max(0, Math.min(1, totalVal / totalMax)) : 0;
+ },
+ };
- // Progress helpers
- function computeAggregateProgress(nodes) {
- let totalMax = 0;
- let totalVal = 0;
- Object.values(nodes || {}).forEach((st) => {
- const mv = Math.max(1, Number((st && st.max) || 1));
- const vv = Math.max(0, Math.min(Number((st && st.value) || 0), mv));
- totalMax += mv;
- totalVal += vv;
- });
- return totalMax > 0 ? Math.max(0, Math.min(1, totalVal / totalMax)) : 0;
- }
+ const UI = {
+ el(tag, attrs = {}, children = []) {
+ const e = document.createElement(tag);
+ for (const [k, v] of Object.entries(attrs)) {
+ if (k === 'class') {
+ e.className = v;
+ } else if (k === 'text') {
+ e.textContent = v;
+ } else {
+ e.setAttribute(k, v);
+ }
+ }
+ for (const c of children) {
+ e.appendChild(c);
+ }
+ return e;
+ },
- function updateProgressBarsFromState() {
- if (!state.container) return;
- Object.entries(state.running_progress).forEach(([pid, f]) => {
- const bar = state.container.querySelector(`.pqueue-item[data-id="${pid}"] .pqueue-progress-bar`);
- if (bar) bar.style.width = `${(Number(f) * 100).toFixed(2)}%`;
- });
- }
+ createButton({ id, text, icon, classes, onClick }) {
+ const button = UI.el('button', { id, class: `p-button p-component ${classes}` });
+ if (icon) {
+ button.appendChild(UI.el('span', { class: `p-button-icon pi ${icon}` }));
+ }
+ if (text) {
+ button.appendChild(UI.el('span', { class: 'p-button-label', text }));
+ }
+ if (onClick) {
+ button.onclick = onClick;
+ }
+ return button;
+ },
- function ensureThemeLink() {
- if (!document.getElementById('pqueue-style-link')) {
- const link = document.createElement('link');
- link.id = 'pqueue-style-link';
- link.rel = 'stylesheet';
- link.href = '/extensions/ComfyUI-PersistentQueue/css/queue_style.css';
- document.head.appendChild(link);
+ createCard(title, content) {
+ return UI.el('div', { class: 'p-card pqueue-card' }, [
+ UI.el('div', { class: 'p-card-body' }, [
+ UI.el('div', { class: 'p-card-title', text: title }),
+ content,
+ ]),
+ ]);
+ },
+
+ updateProgressBarsFromState() {
+ if (!state.container) return;
+ Object.entries(state.running_progress).forEach(([pid, f]) => {
+ const bar = state.container.querySelector(`.pqueue-item[data-id="${pid}"] .pqueue-progress-bar`);
+ if (bar) bar.style.width = `${(Number(f) * 100).toFixed(2)}%`;
+ });
+ },
+
+ ensureThemeLink() {
+ if (!document.getElementById('pqueue-style-link')) {
+ const link = document.createElement('link');
+ link.id = 'pqueue-style-link';
+ link.rel = 'stylesheet';
+ link.href = '/extensions/ComfyUI-PersistentQueue/css/queue_style.css';
+ document.head.appendChild(link);
+ }
+ },
+
+ renderToolbar() {
+ const pauseBtn = UI.createButton({
+ id: 'pqueue-toggle',
+ text: state.paused ? 'Resume' : 'Pause',
+ icon: state.paused ? 'pi-play' : 'pi-pause',
+ classes: state.paused ? 'p-button-success' : 'p-button-warning',
+ });
+ const refreshBtn = UI.createButton({
+ id: 'pqueue-refresh',
+ text: 'Refresh',
+ icon: 'pi-refresh',
+ classes: 'p-button-text',
+ });
+ const clearBtn = UI.createButton({
+ id: 'pqueue-clear',
+ text: 'Clear Pending',
+ icon: 'pi-stop',
+ classes: 'p-button-text p-button-danger',
+ });
+
+ return UI.el('div', { class: 'pqueue-toolbar' }, [pauseBtn, refreshBtn, clearBtn]);
+ },
+
+ renderRunningSection() {
+ const runningList = UI.el('ul', { class: 'pqueue-list' });
+ if (!state.queue_running.length) {
+ runningList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No running job' }));
+ } else {
+ state.queue_running.forEach(item => {
+ const pid = item[1];
+ const frac = Math.max(0, Math.min(1, Number(state.running_progress?.[pid]) || 0));
+ const progressBar = UI.el('div', { class: 'pqueue-progress-bar' });
+ progressBar.style.width = `${(frac * 100).toFixed(2)}%`;
+
+ const li = UI.el('li', { class: 'pqueue-item', 'data-id': pid }, [
+ UI.el('div', { class: 'pqueue-left' }, [
+ UI.el('div', { class: 'pqueue-meta' }, [
+ UI.el('i', { class: 'pi pi-spin pi-spinner' }),
+ UI.el('span', { text: pid })
+ ]),
+ UI.el('div', { class: 'pqueue-progress-wrap' }, [progressBar]),
+ ]),
+ UI.el('div', { class: 'pqueue-actions' }, [])
+ ]);
+ runningList.appendChild(li);
+ });
+ }
+ return UI.createCard('Running', runningList);
+ },
+
+ renderPendingSection() {
+ const pendingList = UI.el('ul', { id: 'pqueue-pending', class: 'pqueue-list' });
+ if (!state.queue_pending.length) {
+ pendingList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No pending items' }));
+ } else {
+ state.queue_pending.forEach(item => {
+ const checked = state.selectedPending.has(item[1]);
+ const checkbox = UI.el('input', { type: 'checkbox', class: 'p-checkbox-input pqueue-select' });
+ if (checked) checkbox.checked = true;
+
+ const priorityInput = UI.el('input', { type: 'number', value: '0', class: 'p-inputtext pqueue-priority', min: '0', style: 'width:5rem' });
+ const savePriorityBtn = UI.createButton({
+ icon: 'pi-sort-amount-up-alt',
+ classes: 'p-button-text pqueue-priority-save',
+ });
+ const deleteBtn = UI.createButton({
+ icon: 'pi-trash',
+ classes: 'p-button-text p-button-danger pqueue-delete',
+ });
+
+ const li = UI.el('li', { draggable: 'true', 'data-id': item[1], class: 'pqueue-item' }, [
+ UI.el('div', { class: 'pqueue-meta' }, [
+ UI.el('i', { class: 'pi pi-bars', style: 'cursor:grab' }),
+ checkbox,
+ UI.el('span', { text: item[1] })
+ ]),
+ UI.el('div', { class: 'pqueue-actions' }, [
+ priorityInput,
+ savePriorityBtn,
+ deleteBtn,
+ ])
+ ]);
+ pendingList.appendChild(li);
+ });
+ }
+ return UI.createCard(`Pending (${state.queue_pending.length})`, pendingList);
+ },
+
+ renderHistorySection() {
+ const historyList = UI.el('ul', { class: 'pqueue-list' });
+ if (!state.history.length) {
+ historyList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No history' }));
+ } else {
+ state.history.forEach(row => {
+ const ok = (row.status ?? 'success') === 'success';
+ const thumbs = UI.buildResultThumbs(row);
+ const time = UI.buildDuration(row);
+ const li = UI.el('li', { class: 'pqueue-item' }, [
+ UI.el('div', { class: 'pqueue-meta' }, [
+ UI.el('i', { class: `pi ${ok ? 'pi-check pqueue-status-success' : 'pi-times pqueue-status-error'}` }),
+ (time ? UI.el('span', { class: 'task-time', text: time }) : UI.el('span', {}))
+ ]),
+ UI.el('div', { class: 'pqueue-thumbs-grid' }, thumbs.children.length ? Array.from(thumbs.children) : [])
+ ]);
+ historyList.appendChild(li);
+ });
+ }
+ return UI.createCard('History', historyList);
+ },
+
+ render() {
+ if (!state.container) return;
+ UI.ensureThemeLink();
+ state.container.innerHTML = '';
+
+ const root = UI.el('div', { class: 'pqueue-root' });
+ const toolbar = UI.renderToolbar();
+ const sections = UI.el('div', { class: 'pqueue-sections' });
+
+ sections.appendChild(UI.renderRunningSection());
+ sections.appendChild(UI.el('div', { class: 'pqueue-divider' }));
+ sections.appendChild(UI.renderPendingSection());
+ sections.appendChild(UI.el('div', { class: 'pqueue-divider' }));
+ sections.appendChild(UI.renderHistorySection());
+
+ root.appendChild(toolbar);
+ root.appendChild(sections);
+ state.container.appendChild(root);
+
+ Events.init();
+ UI.updateProgressBarsFromState();
+ },
+
+ 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;
+ } catch(e) { return UI.el('span', {}); }
+ },
+
+ buildDuration(row) {
+ try {
+ const dur = Number(row.duration_seconds);
+ if (!Number.isNaN(dur) && dur > 0) {
+ return `${dur.toFixed(2)}s`;
+ }
+ const started = row.created_at ?? row.started_at;
+ const completed = row.completed_at;
+ if (!started || !completed) return '';
+ const t1 = new Date(started).getTime();
+ const t2 = new Date(completed).getTime();
+ if (!isFinite(t1) || !isFinite(t2)) return '';
+ const sec = Math.max(0, (t2 - t1) / 1000);
+ return `${sec.toFixed(2)}s`;
+ } 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');
+ },
+
+ ensureStyles() {
+ if (document.getElementById('pqueue-styles')) return;
+ const style = document.createElement('style');
+ style.id = 'pqueue-styles';
+ style.textContent = `
+ #pqueue-fab{position:fixed;right:16px;bottom:16px;z-index:99999;background:#2d2f34;color:#fff;border:none;border-radius:20px;padding:8px 12px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.35)}
+ #pqueue-panel{position:fixed;right:16px;top:64px;z-index:99998;width:420px;max-width:90vw;height:65vh;background:#1c1d20;color:#ddd;border:1px solid #333;border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,0.5);display:none;flex-direction:column}
+ #pqueue-panel header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #333}
+ #pqueue-panel .pqueue-content{padding:8px 12px;overflow:auto;height:100%}
+ #pqueue-panel ul{list-style:none;margin:0;padding:0}
+ #pqueue-panel li{display:flex;gap:8px;align-items:center;padding:6px 4px;border-bottom:1px dashed #2c2c2c}
+ #pqueue-panel .pqueue-item{cursor:grab}
+ #pqueue-panel .pqueue-section{margin-bottom:10px}
+ `;
+ document.head.appendChild(style);
+ },
+
+ mount() {
+ if (state.container) return;
+ UI.ensureStyles();
+ // Floating button
+ const fab = document.createElement('button');
+ fab.id = 'pqueue-fab';
+ fab.textContent = 'Persistent Queue';
+ fab.onclick = () => {
+ const panel = document.getElementById('pqueue-panel');
+ if (!panel) return;
+ const visible = panel.style.display !== 'none';
+ panel.style.display = visible ? 'none' : 'flex';
+ if (!visible) refresh();
+ };
+ document.body.appendChild(fab);
+
+ const panel = UI.el('div', { id: 'pqueue-panel', class: 'pqueue-panel' });
+ const header = document.createElement('header');
+ const title = document.createElement('div');
+ title.textContent = 'Persistent Queue';
+ const close = document.createElement('button');
+ close.textContent = '×';
+ close.onclick = () => { panel.style.display = 'none'; };
+ header.appendChild(title);
+ header.appendChild(close);
+ panel.appendChild(header);
+ state.container = UI.el('div', { class: 'pqueue-content' });
+ panel.appendChild(state.container);
+ document.body.appendChild(panel);
+ refresh();
}
- }
+ };
- function render() {
- if (!state.container) return;
- ensureThemeLink();
- state.container.innerHTML = '';
-
- const root = el('div', { class: 'pqueue-root' });
-
- // Toolbar
- const toolbar = el('div', { class: 'pqueue-toolbar' });
- const pauseBtn = el('button', { id: 'pqueue-toggle', class: `p-button p-component ${state.paused ? 'p-button-success' : 'p-button-warning'}` });
- pauseBtn.innerHTML = `${state.paused ? 'Resume' : 'Pause'}`;
- const refreshBtn = el('button', { id: 'pqueue-refresh', class: 'p-button p-component p-button-text' });
- refreshBtn.innerHTML = `Refresh`;
- const clearBtn = el('button', { id: 'pqueue-clear', class: 'p-button p-component p-button-text p-button-danger' });
- clearBtn.innerHTML = `Clear Pending`;
- toolbar.appendChild(pauseBtn);
- toolbar.appendChild(refreshBtn);
- toolbar.appendChild(clearBtn);
-
- const sections = el('div', { class: 'pqueue-sections' });
-
- // Running
- const runningCard = el('div', { class: 'p-card pqueue-card' });
- const runningBody = el('div', { class: 'p-card-body' });
- runningBody.appendChild(el('div', { class: 'p-card-title', text: 'Running' }));
- const runningList = el('ul', { class: 'pqueue-list' });
- if (!state.queue_running.length) runningList.appendChild(el('li', { class: 'pqueue-empty', text: 'No running job' }));
- state.queue_running.forEach(item => {
- const pid = item[1];
- const leftCol = el('div', { class: 'pqueue-left' }, [
- el('div', { class: 'pqueue-meta' }, [
- el('i', { class: 'pi pi-spin pi-spinner' }),
- el('span', { text: pid })
- ]),
- (() => {
- const wrap = el('div', { class: 'pqueue-progress-wrap' });
- const bar = el('div', { class: 'pqueue-progress-bar' });
- const frac = Math.max(0, Math.min(1, Number(state.running_progress && state.running_progress[pid]) || 0));
- bar.style.width = `${(frac * 100).toFixed(2)}%`;
- wrap.appendChild(bar);
- return wrap;
- })()
- ]);
- const li = el('li', { class: 'pqueue-item', 'data-id': pid }, [
- leftCol,
- el('div', { class: 'pqueue-actions' }, [])
- ]);
- runningList.appendChild(li);
- });
- runningBody.appendChild(runningList);
- runningCard.appendChild(runningBody);
-
- // Pending
- const pendingCard = el('div', { class: 'p-card pqueue-card' });
- const pendingBody = el('div', { class: 'p-card-body' });
- pendingBody.appendChild(el('div', { class: 'p-card-title', text: `Pending (${state.queue_pending.length})` }));
- const pendingList = el('ul', { id: 'pqueue-pending', class: 'pqueue-list' });
- if (!state.queue_pending.length) pendingList.appendChild(el('li', { class: 'pqueue-empty', text: 'No pending items' }));
- state.queue_pending.forEach(item => {
- const checked = state.selectedPending.has(item[1]);
- const li = el('li', { draggable: 'true', 'data-id': item[1], class: 'pqueue-item' }, [
- el('div', { class: 'pqueue-meta' }, [
- el('i', { class: 'pi pi-bars', style: 'cursor:grab' }),
- (() => { const cb = el('input', { type: 'checkbox', class: 'p-checkbox-input pqueue-select' }); if (checked) cb.checked = true; return cb; })(),
- el('span', { text: item[1] })
- ]),
- el('div', { class: 'pqueue-actions' }, [
- el('input', { type: 'number', value: '0', class: 'p-inputtext pqueue-priority', min: '0', style: 'width:5rem' }),
- (() => { const b=el('button', { class: 'p-button p-component p-button-text pqueue-priority-save' }); b.innerHTML=''; return b; })(),
- (() => { const b=el('button', { class: 'p-button p-component p-button-text p-button-danger pqueue-delete' }); b.innerHTML=''; return b; })(),
- ])
- ]);
- pendingList.appendChild(li);
- });
- pendingBody.appendChild(pendingList);
- pendingCard.appendChild(pendingBody);
-
- // History
- const historyCard = el('div', { class: 'p-card pqueue-card' });
- const historyBody = el('div', { class: 'p-card-body' });
- historyBody.appendChild(el('div', { class: 'p-card-title', text: 'History' }));
- const historyList = el('ul', { class: 'pqueue-list' });
- if (!state.history.length) historyList.appendChild(el('li', { class: 'pqueue-empty', text: 'No history' }));
- state.history.forEach(row => {
- const ok = (row.status||'success') === 'success';
- const thumbs = buildResultThumbs(row);
- const time = buildDuration(row);
- const li = el('li', { class: 'pqueue-item' }, [
- el('div', { class: 'pqueue-meta' }, [
- el('i', { class: `pi ${ok ? 'pi-check pqueue-status-success' : 'pi-times pqueue-status-error'}` }),
- (time ? el('span', { class: 'task-time', text: time }) : el('span', {}))
- ]),
- el('div', { class: 'pqueue-thumbs-grid' }, thumbs.children.length ? Array.from(thumbs.children) : [])
- ]);
- historyList.appendChild(li);
- });
- historyBody.appendChild(historyList);
- historyCard.appendChild(historyBody);
-
- sections.appendChild(runningCard);
- sections.appendChild(el('div', { class: 'pqueue-divider' }));
- sections.appendChild(pendingCard);
- sections.appendChild(el('div', { class: 'pqueue-divider' }));
- sections.appendChild(historyCard);
-
- root.appendChild(toolbar);
- root.appendChild(sections);
- state.container.appendChild(root);
-
- wireHandlers();
- updateProgressBarsFromState();
- }
-
- function wireHandlers() {
- document.getElementById('pqueue-toggle').onclick = async () => {
+ const Events = {
+ async onPauseClick() {
if (state.paused) await API.resume(); else await API.pause();
await refresh();
- };
- document.getElementById('pqueue-refresh').onclick = refresh;
- const clearBtn = document.getElementById('pqueue-clear');
- if (clearBtn) {
- clearBtn.onclick = async () => {
- const ids = Array.from(document.querySelectorAll('#pqueue-pending li[data-id]')).map(li => li.dataset.id);
- if (ids.length) {
- await API.del(ids);
- await refresh();
- }
- };
- }
+ },
- const list = document.getElementById('pqueue-pending');
- let dragged;
- list.addEventListener('dragstart', (e) => {
+ async onClearClick() {
+ const ids = Array.from(document.querySelectorAll('#pqueue-pending li[data-id]')).map(li => li.dataset.id);
+ if (ids.length) {
+ await API.del(ids);
+ await refresh();
+ }
+ },
+
+ onPendingListItemCheckboxChange(e) {
const li = e.target.closest('li');
- dragged = li;
- e.dataTransfer.setData('text/plain', li.dataset.id);
- });
- list.addEventListener('dragover', (e) => e.preventDefault());
- list.addEventListener('drop', async (e) => {
- e.preventDefault();
- const target = e.target.closest('li');
- if (!dragged || !target || dragged === target) return;
- const rect = target.getBoundingClientRect();
- const before = (e.clientY - rect.top) < rect.height / 2;
- if (before) list.insertBefore(dragged, target); else list.insertBefore(dragged, target.nextSibling);
- const order = Array.from(list.querySelectorAll('li')).map(li => li.dataset.id);
- await API.reorder(order);
+ const id = li.dataset.id;
+ if (e.target.checked) state.selectedPending.add(id); else state.selectedPending.delete(id);
+ },
+
+ async onPendingListItemPrioritySaveClick(e) {
+ const li = e.target.closest('li');
+ const id = li.dataset.id;
+ const val = parseInt(li.querySelector('.pqueue-priority').value || '0', 10);
+ await API.setPriority(id, val);
await refresh();
- });
+ },
- list.querySelectorAll('.pqueue-select').forEach(cb => {
- cb.onchange = (e) => {
- const li = e.target.closest('li');
- const id = li.dataset.id;
- if (e.target.checked) state.selectedPending.add(id); else state.selectedPending.delete(id);
- };
- });
+ async onPendingListItemDeleteClick(e) {
+ const li = e.target.closest('li');
+ const id = li.dataset.id;
+ await API.del([id]);
+ await refresh();
+ },
- list.querySelectorAll('.pqueue-priority-save').forEach(btn => {
- btn.onclick = async (e) => {
- const li = e.target.closest('li');
- const id = li.dataset.id;
- const val = parseInt(li.querySelector('.pqueue-priority').value || '0', 10);
- await API.setPriority(id, val);
- await refresh();
- };
- });
-
- list.querySelectorAll('.pqueue-delete').forEach(btn => {
- btn.onclick = async (e) => {
- const li = e.target.closest('li');
- const id = li.dataset.id;
- await API.del([id]);
- await refresh();
- };
- });
- }
-
- function buildResultThumbs(row) {
- try {
- const wrap = el('div', { class: 'flex gap-1 flex-wrap' });
- let outputs = row.outputs || {};
- if (typeof outputs === 'string') {
- try { outputs = JSON.parse(outputs); } catch(e) { outputs = {}; }
+ init() {
+ document.getElementById('pqueue-toggle').onclick = Events.onPauseClick;
+ document.getElementById('pqueue-refresh').onclick = refresh;
+ const clearBtn = document.getElementById('pqueue-clear');
+ if (clearBtn) {
+ clearBtn.onclick = Events.onClearClick;
}
- const images = [];
- Object.values(outputs).forEach(v => {
- if (v && typeof v === 'object') {
- const imgs = (v.images) || (v.ui && v.ui.images) || [];
- if (Array.isArray(imgs)) imgs.forEach(i => images.push(i));
- } else if (Array.isArray(v)) {
- v.forEach(i => { if (i && (i.filename || i.name)) images.push(i); });
+
+ const list = document.getElementById('pqueue-pending');
+ if (!list) return;
+
+ let dragged;
+ list.addEventListener('dragstart', (e) => {
+ const li = e.target.closest('li');
+ dragged = li;
+ e.dataTransfer.setData('text/plain', li.dataset.id);
+ });
+ list.addEventListener('dragover', (e) => e.preventDefault());
+ list.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ const target = e.target.closest('li');
+ if (!dragged || !target || dragged === target) return;
+ const rect = target.getBoundingClientRect();
+ const before = (e.clientY - rect.top) < rect.height / 2;
+ if (before) list.insertBefore(dragged, target); else list.insertBefore(dragged, target.nextSibling);
+ const order = Array.from(list.querySelectorAll('li')).map(li => li.dataset.id);
+ await API.reorder(order);
+ await refresh();
+ });
+
+ list.addEventListener('change', (e) => {
+ if (e.target.classList.contains('pqueue-select')) {
+ Events.onPendingListItemCheckboxChange(e);
}
});
- images.slice(0, 4).forEach(i => {
- const filename = i.filename || i.name || '';
- const type = i.type || 'output';
- const subfolder = i.subfolder || '';
- const url = `/view?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}${subfolder?`&subfolder=${encodeURIComponent(subfolder)}`:''}&preview=webp;50`;
- const img = el('img', { src: url, class: 'pqueue-thumb', title: filename });
- img.onclick = () => openGallery(filename, type, subfolder);
- wrap.appendChild(img);
+
+ list.addEventListener('click', (e) => {
+ if (e.target.closest('.pqueue-priority-save')) {
+ Events.onPendingListItemPrioritySaveClick(e);
+ } else if (e.target.closest('.pqueue-delete')) {
+ Events.onPendingListItemDeleteClick(e);
+ }
});
- return wrap;
- } catch(e) { return el('span', {}); }
- }
-
- function buildDuration(row) {
- try {
- const dur = Number(row.duration_seconds);
- if (!Number.isNaN(dur) && dur > 0) {
- return `${dur.toFixed(2)}s`;
- }
- const started = row.created_at || row.started_at;
- const completed = row.completed_at;
- if (!started || !completed) return '';
- const t1 = new Date(started).getTime();
- const t2 = new Date(completed).getTime();
- if (!isFinite(t1) || !isFinite(t2)) return '';
- const sec = Math.max(0, (t2 - t1) / 1000);
- return `${sec.toFixed(2)}s`;
- } catch(e) { return ''; }
- }
-
- function openGallery(filename, type, subfolder) {
- try {
- if (window.app && window.app.extensionManager && window.app.extensionManager.dialog) {
- // Use the built-in image preview route; the core gallery opens via dialogs in the frontend
- const url = `/view?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}${subfolder?`&subfolder=${encodeURIComponent(subfolder)}`:''}`;
- window.open(url, '_blank');
- return;
- }
- } catch(e) {}
- const url = `/view?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}${subfolder?`&subfolder=${encodeURIComponent(subfolder)}`:''}`;
- window.open(url, '_blank');
- }
+ }
+ };
async function refresh() {
const q = await API.getQueue();