- 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
222 lines
8.1 KiB
JavaScript
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);
|
|
}
|
|
}); |