743 lines
30 KiB
JavaScript
743 lines
30 KiB
JavaScript
// Minimal frontend extension for ComfyUI Persistent Queue
|
||
// Loaded automatically from WEB_DIRECTORY by ComfyUI and served at /extensions/<name>/pqueue.js
|
||
|
||
(function() {
|
||
const API = {
|
||
getQueue: () => fetch('/api/pqueue').then(r => r.json()),
|
||
getHistory: (limit=50) => fetch(`/api/pqueue/history?limit=${limit}`).then(r => r.json()),
|
||
pause: () => fetch('/api/pqueue/pause', { method: 'POST' }),
|
||
resume: () => fetch('/api/pqueue/resume', { method: 'POST' }),
|
||
reorder: (order) => fetch('/api/pqueue/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order }) }),
|
||
setPriority: (prompt_id, priority) => fetch('/api/pqueue/priority', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt_id, priority }) }),
|
||
del: (prompt_ids) => fetch('/api/pqueue/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt_ids }) }),
|
||
};
|
||
|
||
const state = {
|
||
paused: false,
|
||
queue_running: [],
|
||
queue_pending: [],
|
||
db_pending: [],
|
||
history: [],
|
||
running_progress: {},
|
||
container: null,
|
||
listPending: null,
|
||
selectedPending: new Set(),
|
||
};
|
||
|
||
// Polling control (fallback only)
|
||
let _pqueuePollIntervalId = null;
|
||
let _pqueueFocusListener = null;
|
||
|
||
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;
|
||
},
|
||
};
|
||
|
||
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;
|
||
},
|
||
|
||
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;
|
||
},
|
||
|
||
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 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, 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();
|
||
},
|
||
|
||
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);
|
||
const imgCount = UI.countImages(row);
|
||
if (imgCount > 1) {
|
||
const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${imgCount}` });
|
||
thumbWrap.appendChild(badge);
|
||
}
|
||
if (galleryImages.length) {
|
||
thumbWrap.onclick = () => UI.openGallery(galleryImages, 0, row.prompt_id);
|
||
} else {
|
||
thumbWrap.onclick = null;
|
||
}
|
||
container.appendChild(thumbWrap);
|
||
}
|
||
// Fallback to live preview route if DB thumb missing
|
||
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);
|
||
}
|
||
}
|
||
if (!container.children.length) ensurePlaceholder();
|
||
return container;
|
||
} catch(e) { return UI.el('span', {}); }
|
||
},
|
||
|
||
countImages(row) {
|
||
try {
|
||
return UI.extractImages(row).length;
|
||
} catch(e) { return 0; }
|
||
},
|
||
|
||
buildPreviewUrl(imgDesc, pid) {
|
||
const url = new URL('/api/pqueue/preview', window.location.origin);
|
||
url.searchParams.set('filename', imgDesc.filename);
|
||
url.searchParams.set('type', imgDesc.type);
|
||
if (imgDesc.subfolder) url.searchParams.set('subfolder', imgDesc.subfolder);
|
||
url.searchParams.set('preview', 'webp;50');
|
||
if (pid) url.searchParams.set('pid', pid);
|
||
return url;
|
||
},
|
||
|
||
// 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);
|
||
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 ''; }
|
||
},
|
||
|
||
// Lightbox-style gallery for image batches
|
||
openGallery(images, startIndex = 0, pid = null) {
|
||
try {
|
||
UI.ensureGalleryElements();
|
||
UI._galleryState.images = images || [];
|
||
UI._galleryState.pid = pid;
|
||
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 = UI.buildPreviewUrl(imgDesc, UI._galleryState.pid);
|
||
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;
|
||
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);
|
||
}
|
||
};
|
||
|
||
const Events = {
|
||
async onPauseClick() {
|
||
if (state.paused) await API.resume(); else await API.pause();
|
||
await refresh();
|
||
},
|
||
|
||
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');
|
||
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();
|
||
},
|
||
|
||
async onPendingListItemDeleteClick(e) {
|
||
const li = e.target.closest('li');
|
||
const id = li.dataset.id;
|
||
await API.del([id]);
|
||
await refresh();
|
||
},
|
||
|
||
init() {
|
||
document.getElementById('pqueue-toggle').onclick = Events.onPauseClick;
|
||
const clearBtn = document.getElementById('pqueue-clear');
|
||
if (clearBtn) {
|
||
clearBtn.onclick = Events.onClearClick;
|
||
}
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
list.addEventListener('click', (e) => {
|
||
if (e.target.closest('.pqueue-priority-save')) {
|
||
Events.onPendingListItemPrioritySaveClick(e);
|
||
} else if (e.target.closest('.pqueue-delete')) {
|
||
Events.onPendingListItemDeleteClick(e);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
async function refresh() {
|
||
const q = await API.getQueue();
|
||
state.paused = !!q.paused;
|
||
state.queue_running = q.queue_running || [];
|
||
state.queue_pending = q.queue_pending || [];
|
||
state.db_pending = q.db_pending || [];
|
||
state.running_progress = q.running_progress || {};
|
||
const h = await API.getHistory(50);
|
||
state.history = h.history || [];
|
||
UI.render();
|
||
}
|
||
|
||
function setupSocketListeners() {
|
||
try {
|
||
const api = window.app?.api;
|
||
if (!api || typeof api.addEventListener !== 'function') {
|
||
console.warn('pqueue: WebSocket API not found, falling back to polling.');
|
||
return false;
|
||
}
|
||
|
||
const onProgressState = (ev) => {
|
||
try {
|
||
const payload = ev?.detail;
|
||
if (!payload?.prompt_id || !payload.nodes) return;
|
||
state.running_progress[payload.prompt_id] = Progress.computeAggregateProgress(payload.nodes);
|
||
UI.updateProgressBarsFromState();
|
||
} catch (e) { /* ignore */ }
|
||
};
|
||
|
||
// Use immediate refresh on lifecycle events; progress updates are handled separately
|
||
const onQueueOrExecChange = () => refresh();
|
||
const onExecDone = () => refresh();
|
||
|
||
api.addEventListener('progress_state', onProgressState);
|
||
api.addEventListener('status', onQueueOrExecChange);
|
||
// 'executing' fires often per-node; do not refresh on every tick
|
||
api.addEventListener('executing', onProgressState);
|
||
api.addEventListener('executed', onExecDone);
|
||
api.addEventListener('execution_start', onQueueOrExecChange);
|
||
api.addEventListener('execution_success', onExecDone);
|
||
api.addEventListener('execution_error', onExecDone);
|
||
api.addEventListener('execution_interrupted', onExecDone);
|
||
|
||
// If we reach here, socket listeners are active. Ensure fallback polling is disabled.
|
||
stopFallbackPolling();
|
||
return true;
|
||
} catch (e) {
|
||
console.error('pqueue: Error setting up WebSocket listeners.', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function stopFallbackPolling() {
|
||
try {
|
||
if (_pqueuePollIntervalId) {
|
||
clearInterval(_pqueuePollIntervalId);
|
||
_pqueuePollIntervalId = null;
|
||
}
|
||
if (_pqueueFocusListener) {
|
||
window.removeEventListener('focus', _pqueueFocusListener);
|
||
_pqueueFocusListener = null;
|
||
}
|
||
} catch (e) { /* noop */ }
|
||
}
|
||
|
||
function setupFallbackPolling() {
|
||
// Only used when WebSocket API is unavailable
|
||
stopFallbackPolling();
|
||
_pqueueFocusListener = () => refresh();
|
||
window.addEventListener('focus', _pqueueFocusListener);
|
||
_pqueuePollIntervalId = setInterval(refresh, 3000);
|
||
}
|
||
|
||
function initialize() {
|
||
|
||
const finalSetup = (el) => {
|
||
state.container = el;
|
||
UI.render();
|
||
if (!setupSocketListeners()) {
|
||
setupFallbackPolling();
|
||
// Retry establishing WebSocket listeners in the background
|
||
let retries = 50;
|
||
const retryId = setInterval(() => {
|
||
if (setupSocketListeners() || --retries <= 0) {
|
||
clearInterval(retryId);
|
||
}
|
||
}, 200);
|
||
}
|
||
// Initial fetch to populate queue + history on first load
|
||
refresh();
|
||
};
|
||
|
||
const sidebarTabDefinition = {
|
||
id: 'persistent_queue',
|
||
icon: 'pi pi-history',
|
||
title: 'Persistent Queue',
|
||
tooltip: 'Persistent Queue',
|
||
type: 'custom',
|
||
render: (el) => {
|
||
document.getElementById('pqueue-fab')?.remove();
|
||
document.getElementById('pqueue-panel')?.remove();
|
||
finalSetup(el);
|
||
},
|
||
};
|
||
|
||
const tryRegisterSidebar = () => {
|
||
try {
|
||
if (window.app?.extensionManager?.registerSidebarTab) {
|
||
window.app.extensionManager.registerSidebarTab(sidebarTabDefinition);
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.error("pqueue: Error registering sidebar tab.", error);
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const initLogic = () => {
|
||
|
||
if (tryRegisterSidebar()) {
|
||
return;
|
||
}
|
||
|
||
if (!document.getElementById('pqueue-panel')) {
|
||
UI.mount();
|
||
finalSetup(state.container);
|
||
}
|
||
|
||
let retries = 50; // Try for 10 seconds
|
||
const intervalId = setInterval(() => {
|
||
if (tryRegisterSidebar() || --retries <= 0) {
|
||
clearInterval(intervalId);
|
||
}
|
||
}, 200);
|
||
};
|
||
|
||
const extension = {
|
||
name: 'ComfyUI-PersistentQueue',
|
||
async setup(app) {
|
||
initLogic();
|
||
}
|
||
};
|
||
|
||
// Use the modern extension system if available.
|
||
if (window.app?.registerExtension) {
|
||
window.app.registerExtension(extension);
|
||
} else {
|
||
// Fallback for very old versions.
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initLogic);
|
||
} else {
|
||
initLogic();
|
||
}
|
||
}
|
||
}
|
||
|
||
initialize();
|
||
})();
|
||
|
||
|