Files
marcoc2-ComfyUI-AnotherUtils/js/caption_image_loader.js
T

350 lines
15 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "AnotherUtils.CaptionImageLoader",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "CaptionImageLoader") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.captionData = [];
this.selectedBasename = "";
this.statusMessage = "No images loaded. Set specific path.";
// Scrolling state
this.scrollY = 0;
this.itemHeight = 110;
this.listStartY = 140;
// Helper to fetch images
this.fetchImages = async () => {
const dirWidget = this.widgets.find(w => w.name === "directory");
if (!dirWidget || !dirWidget.value) {
this.statusMessage = "Please set a directory path.";
this.setDirtyCanvas(true);
return;
}
this.statusMessage = "Loading...";
this.setDirtyCanvas(true);
try {
const response = await api.fetchApi(`/another_utils/list_images?directory=${encodeURIComponent(dirWidget.value)}`);
if (response.status !== 200) {
throw new Error(response.statusText);
}
const data = await response.json();
if (data.error) {
this.statusMessage = "Error: " + data.error;
this.setDirtyCanvas(true);
return;
}
this.captionData = data.files || [];
if (this.captionData.length === 0) {
this.statusMessage = "No images found in directory.";
} else {
this.statusMessage = "";
}
// Reset scroll
this.scrollY = 0;
// We don't auto-resize infinitely anymore, we fix height to something reasonable or let user resize
// But to be nice, let's set a min height if small
this.setSize([this.size[0], Math.max(this.size[1], 500)]);
this.setDirtyCanvas(true);
} catch (e) {
this.statusMessage = "Error loading: " + e.message;
this.setDirtyCanvas(true);
}
};
// Add "Load List" button
this.addWidget("button", "Load Images / Refresh", null, () => {
this.fetchImages();
});
// Add callback to directory widget for auto-load
const dirWidget = this.widgets.find(w => w.name === "directory");
if (dirWidget) {
const originalCallback = dirWidget.callback;
dirWidget.callback = (value) => {
if (originalCallback) originalCallback(value);
this.fetchImages();
};
}
const selWidget = this.widgets.find(w => w.name === "selected_basename");
if (selWidget) {
selWidget.computeSize = () => [0, -4];
}
return r;
};
// Scrollbar state
this.isDraggingScroll = false;
this.lastMouseY = 0;
// Input Handling
const onMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (event, pos, graphCanvas) {
const r = onMouseDown ? onMouseDown.apply(this, arguments) : undefined;
if (!this.captionData || this.captionData.length === 0) return r;
const x = pos[0];
const y = pos[1];
// Helper to get scrollbar rect
const w = this.size[0];
const viewHeight = this.size[1] - this.listStartY;
const totalHeight = this.captionData.length * this.itemHeight;
const sbWidth = 10; // Wider for easier grab
const sbX = w - sbWidth - 2;
// Check Scrollbar click
if (totalHeight > viewHeight && x >= sbX && x <= sbX + sbWidth && y >= this.listStartY) {
this.isDraggingScroll = true;
this.lastMouseY = y;
console.log("[CaptionImageLoader] Scrollbar drag start");
// Capture input
if (graphCanvas.canvas) {
// This ensures we get mouse moves even outside node
// LiteGraph doesn't always expose setCapture easily, but we can rely on graphCanvas.dragging_canvas = false
}
return true;
}
if (y < this.listStartY) return r;
// Item Click
// Adjust index calculation correctly
const index = Math.floor((y - this.listStartY - this.scrollY) / this.itemHeight);
if (index >= 0 && index < this.captionData.length) {
this.selectedBasename = this.captionData[index].basename;
const widget = this.widgets.find(w => w.name === "selected_basename");
if (widget) {
widget.value = this.selectedBasename;
}
this.setDirtyCanvas(true);
return true;
}
return r;
};
const onMouseMove = nodeType.prototype.onMouseMove;
nodeType.prototype.onMouseMove = function (event, pos, graphCanvas) {
if (this.isDraggingScroll) {
const y = pos[1];
const dy = y - this.lastMouseY;
this.lastMouseY = y;
const totalHeight = this.captionData.length * this.itemHeight;
const viewHeight = this.size[1] - this.listStartY;
// Map mouse delta to scroll delta
// Scrollbar moves by (viewHeight / totalHeight) * scrollDelta?
// No, we are moving the thumb.
// Thumb range = sbHeight - thumbHeight. Content range = totalHeight - viewHeight.
// ratio = contentRange / thumbRange.
const sbHeight = viewHeight;
const thumbHeight = Math.max(20, (viewHeight / totalHeight) * viewHeight);
const scrollRange = totalHeight - viewHeight;
const thumbRange = sbHeight - thumbHeight;
if (thumbRange > 0) {
const scrollDelta = -(dy * (scrollRange / thumbRange));
this.scrollY += scrollDelta;
this.scrollY = Math.max(-scrollRange, Math.min(0, this.scrollY));
}
this.setDirtyCanvas(true);
return true;
}
return onMouseMove ? onMouseMove.apply(this, arguments) : undefined;
};
const onMouseUp = nodeType.prototype.onMouseUp;
nodeType.prototype.onMouseUp = function (event, pos, graphCanvas) {
if (this.isDraggingScroll) {
this.isDraggingScroll = false;
console.log("[CaptionImageLoader] Scrollbar drag end");
this.setDirtyCanvas(true);
return true;
}
return onMouseUp ? onMouseUp.apply(this, arguments) : undefined;
};
// Wheel Handling
// Note: LiteGraph nodes should contain onMouseWheel or getExtraMenuOptions
// If checking "pos" is problematic, trust the event.
nodeType.prototype.onWheel = function (event) {
// Older LiteGraph support
console.log("onWheel");
};
// This is the correct method name for ComfyUI's graph
nodeType.prototype.onMouseWheel = function (event, pos, graphCanvas) {
// pos is relative to node top-left
if (!this.captionData || this.captionData.length === 0) return false;
const x = pos[0];
const y = pos[1];
// Only scroll if over the list area
if (y > this.listStartY && x < this.size[0]) {
// Standard deltaY is like 100 or -100.
const delta = event.deltaY;
// Invert logic: Scroll down (positive delta) -> Move content up (negative scrollY)
this.scrollY -= delta * 0.5; // Scale speed if needed
const totalHeight = this.captionData.length * this.itemHeight;
const viewHeight = this.size[1] - this.listStartY;
const minScroll = Math.min(0, -(totalHeight - viewHeight) - 20);
this.scrollY = Math.max(minScroll, Math.min(0, this.scrollY));
this.setDirtyCanvas(true);
return true; // Consume event
}
return false;
}
// Custom Drawing
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground ? onDrawForeground.apply(this, arguments) : undefined;
const margin = 10;
let y = this.listStartY;
const w = this.size[0];
ctx.save();
// Clip content area
ctx.beginPath();
ctx.rect(0, y, w, this.size[1] - y);
ctx.clip();
// Status
if (this.statusMessage) {
ctx.fillStyle = "#ffaaaa";
if (this.statusMessage.startsWith("Loading")) ctx.fillStyle = "#aaaaff";
if (this.statusMessage.startsWith("No images")) ctx.fillStyle = "#ffffaa";
ctx.font = "italic 14px Arial";
ctx.fillText(this.statusMessage, margin, y + 20 + this.scrollY);
}
if (!this.captionData || this.captionData.length === 0) {
ctx.restore();
return;
}
// Optimization: Only draw visible items
// Item Top = listStartY + i*h + scrollY
// Visible range: [listStartY, size[1]]
const totalHeight = this.captionData.length * this.itemHeight;
// Which index starts at top?
// i*h + scrollY >= 0 (relative to list start) -> i*h >= -scrollY -> i >= -scrollY/h
const startIndex = Math.max(0, Math.floor(-this.scrollY / this.itemHeight));
// End index
// i*h + scrollY <= viewHeight
const viewHeight = this.size[1] - this.listStartY;
const endIndex = Math.min(this.captionData.length, Math.ceil((-this.scrollY + viewHeight) / this.itemHeight));
for (let i = startIndex; i < endIndex; i++) {
const item = this.captionData[i];
const itemY = y + (i * this.itemHeight) + this.scrollY;
const isSelected = item.basename === this.selectedBasename;
// Background
ctx.fillStyle = isSelected ? "#445" : (i % 2 === 0 ? "#222" : "#2a2a2a");
ctx.fillRect(margin, itemY, w - margin * 2, this.itemHeight);
if (isSelected) {
ctx.strokeStyle = "#4a9";
ctx.lineWidth = 2;
ctx.strokeRect(margin, itemY, w - margin * 2, this.itemHeight);
}
// Thumbnail
if (!item.imgObj) {
item.imgObj = new Image();
item.imgObj.src = item.thumbnail;
}
if (item.imgObj.complete) {
const scale = Math.min(100 / item.imgObj.width, 100 / item.imgObj.height);
const dw = item.imgObj.width * scale;
const dh = item.imgObj.height * scale;
ctx.drawImage(item.imgObj, margin + 5, itemY + 5, dw, dh);
}
// Utils for Text
const textX = margin + 115;
const textMaxWidth = w - margin * 3 - 115;
// Filename
ctx.fillStyle = "#ddd";
ctx.font = "bold 12px Arial";
ctx.fillText(item.filename, textX, itemY + 20);
// Caption
ctx.font = "12px Arial";
const caption = item.caption || "";
const words = caption.split(" ");
let line = "";
let ly = itemY + 40;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + " ";
const metrics = ctx.measureText(testLine);
if (metrics.width > textMaxWidth && n > 0) {
ctx.fillText(line, textX, ly);
line = words[n] + " ";
ly += 15;
if (ly > itemY + this.itemHeight - 5) break;
} else {
line = testLine;
}
}
ctx.fillText(line, textX, ly);
}
ctx.restore();
// Scrollbar
if (totalHeight > viewHeight) {
const sbWidth = 6;
const sbX = w - sbWidth - 2;
const sbY = y;
const sbHeight = viewHeight;
ctx.fillStyle = "#111";
ctx.fillRect(sbX, sbY, sbWidth, sbHeight);
const thumbHeight = Math.max(20, (viewHeight / totalHeight) * viewHeight);
const scrollRatio = -this.scrollY / (totalHeight - viewHeight); // 0 to 1
const thumbY = sbY + scrollRatio * (sbHeight - thumbHeight);
ctx.fillStyle = "#666";
ctx.fillRect(sbX, thumbY, sbWidth, thumbHeight);
}
};
}
},
});