350 lines
15 KiB
JavaScript
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);
|
|
}
|
|
};
|
|
}
|
|
},
|
|
});
|