Files
hermankwan-ComfyUI-Batch-Im…/web/batch_image_timer.js
T
Herman Kwan 82336a25be feat(core): release v0.2.0 major architecture refactor and UI overhaul
- Migrate frontend to modular ES modules (timer, progress, renderer)
- Rewrite batch tracking logic to support Sequential Image Loaders and native batches
- Introduce robust queue-empty fallback debounce to prevent infinite timers
- Decouple timer interactions from progress counters for absolute independence
- Redesign UI layout, fix node resizing conflicts, and add dynamic status colors
- Update README with new usage instructions and v0.2.0 changelog
2026-10-22 23:47:42 -07:00

222 lines
8.1 KiB
JavaScript

import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { TimerEngine } from "./timer_engine.js";
import { ProgressEngine } from "./progress_engine.js";
import { renderTimerUI } from "./ui_renderer.js";
const timer = new TimerEngine();
const progress = new ProgressEngine();
let activeNodeId = null;
let isMacroRunning = false;
let lastCompletionTime = 0;
let queueEmptyTimeout = null;
setInterval(() => {
if (timer.isRunning && !timer.isPaused) {
app.graph?.setDirtyCanvas(true, true);
}
}, 100);
app.registerExtension({
name: "BatchImageTimer.Final",
async setup() {
api.addEventListener("batch_timer_update", (event) => {
const d = event.detail || {};
if (d.node_id) activeNodeId = String(d.node_id);
if (queueEmptyTimeout) {
clearTimeout(queueEmptyTimeout);
queueEmptyTimeout = null;
}
const now = Date.now();
if (!isMacroRunning && (now - lastCompletionTime < 200)) {
return;
}
if (!isMacroRunning) {
isMacroRunning = true;
timer.start();
progress.start(d.total || 1);
}
progress.update(d);
app.graph?.setDirtyCanvas(true, true);
});
api.addEventListener("execution_success", () => {
if (isMacroRunning && progress.current >= progress.total) {
isMacroRunning = false;
lastCompletionTime = Date.now();
timer.stop();
progress.markCompleted();
app.graph?.setDirtyCanvas(true, true);
}
});
api.addEventListener("status", (event) => {
const queueRemaining = event.detail?.status?.exec_info?.queue_remaining;
if (queueRemaining === 0 && isMacroRunning) {
if (queueEmptyTimeout) clearTimeout(queueEmptyTimeout);
queueEmptyTimeout = setTimeout(() => {
if (isMacroRunning) {
isMacroRunning = false;
lastCompletionTime = Date.now();
timer.stop();
if (progress.current < progress.total) {
progress.status = "INTERRUPT";
} else {
progress.markCompleted();
}
fetch("/batch_timer/reset", { method: "POST" }).catch(() => {});
app.graph?.setDirtyCanvas(true, true);
}
queueEmptyTimeout = null;
}, 1000);
}
});
api.addEventListener("execution_error", () => {
if (isMacroRunning) {
setTimeout(() => {
if (isMacroRunning) {
isMacroRunning = false;
progress.status = "ERROR";
timer.stop();
fetch("/batch_timer/reset", { method: "POST" }).catch(() => {});
app.graph?.setDirtyCanvas(true, true);
}
}, 500);
}
});
api.addEventListener("execution_interrupted", () => {
if (isMacroRunning) {
setTimeout(() => {
if (isMacroRunning) {
isMacroRunning = false;
progress.status = "INTERRUPT";
timer.stop();
fetch("/batch_timer/reset", { method: "POST" }).catch(() => {});
app.graph?.setDirtyCanvas(true, true);
}
}, 500);
}
});
},
async nodeCreated(node) {
if (node.comfyClass !== "BatchImageTimer") return;
node.properties = node.properties || {};
node.properties.activeColor = node.properties.activeColor || "#FFFFFF";
node._lastDigitClick = 0;
if (!node._hasInitialSized) {
node._hasInitialSized = true;
requestAnimationFrame(() => {
if (!node.widgets || node.widgets.length === 0) return;
const w = node.widgets[0];
const widgetBottom = (w.last_y !== undefined) ? w.last_y + (w.computeSize ? w.computeSize()[1] : 24) : 60;
const minHeight = widgetBottom + 15 + 115;
if (!node.size || node.size[1] < minHeight) {
node.size = [Math.max(node.size ? node.size[0] : 280, 280), minHeight];
app.graph?.setDirtyCanvas(true, true);
}
});
}
node.onDrawBackground = function (ctx) {
const isThisNodeActive = (activeNodeId === String(this.id));
let timerMs = timer.getElapsedMs();
let displayText = progress.getDisplayText();
let isCompleted = progress.isCompleted;
if (!isThisNodeActive) {
if (!timer.isRunning && progress.isCompleted) {
if (activeNodeId !== String(this.id)) {
timerMs = 0;
displayText = "READY";
isCompleted = false;
}
} else {
timerMs = 0;
displayText = "READY";
isCompleted = false;
}
}
const activeColor = this.properties.activeColor;
renderTimerUI(ctx, this, timerMs, displayText, activeColor, isCompleted);
};
node.onMouseDown = function (e, pos) {
const isThisNodeActive = (activeNodeId === String(this.id)) || !timer.isRunning;
if (this._hitSwatches) {
for (const s of this._hitSwatches) {
if (pos[0] >= s.x && pos[0] <= s.x + s.w && pos[1] >= s.y && pos[1] <= s.y + s.h) {
this.properties.activeColor = s.color;
app.graph.setDirtyCanvas(true, true);
return true;
}
}
}
if (!isThisNodeActive) return false;
const d = this._hitDigit;
if (d && pos[0] >= d.x && pos[0] <= d.x + d.w && pos[1] >= d.y && pos[1] <= d.y + d.h) {
const now = Date.now();
if (now - this._lastDigitClick < 300) {
this._lastDigitClick = 0;
const keepRunning = timer.isRunning;
timer.reset(keepRunning);
if (!keepRunning) {
progress.resetAll();
activeNodeId = null;
}
} else {
this._lastDigitClick = now;
if (timer.isRunning) {
timer.isPaused ? timer.resume() : timer.pause();
}
}
app.graph.setDirtyCanvas(true, true);
return true;
}
};
node.onMouseMove = function (e, pos) {
const canvasEl = app.canvas?.canvas;
if (!canvasEl) return;
const currentCursor = canvasEl.style.cursor || "";
if (currentCursor.includes("resize")) return;
let cursor = "crosshair";
if (this._hitSwatches?.some(s => pos[0] >= s.x && pos[0] <= s.x + s.w && pos[1] >= s.y && pos[1] <= s.y + s.h)) {
cursor = "default";
} else if (this._hitDigit && pos[0] >= this._hitDigit.x && pos[0] <= this._hitDigit.x + this._hitDigit.w && pos[1] >= this._hitDigit.y && pos[1] <= this._hitDigit.y + this._hitDigit.h) {
cursor = "pointer";
}
if (canvasEl.style.cursor !== cursor) {
canvasEl.style.cursor = cursor;
}
};
node.onMouseLeave = function () {
const canvasEl = app.canvas?.canvas;
if (canvasEl) canvasEl.style.cursor = "crosshair";
};
},
async loadedGraphNode(node) {
this.nodeCreated(node);
}
});