Files

483 lines
20 KiB
JavaScript

import { app } from "../../scripts/app.js";
app.registerExtension({
name: "RaykoStudio.ImageText",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "LoadImageWithText") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
node.data = {
mode: "read",
prefix: "Civitai/prompt",
text: "",
selected_image: ""
};
const MIN_WIDTH = 360;
const MIN_HEIGHT = 520;
const PREVIEW_FIXED_HEIGHT = 150;
node.MIN_WIDTH = MIN_WIDTH;
node.MIN_HEIGHT = MIN_HEIGHT;
node.setSize([MIN_WIDTH, MIN_HEIGHT]);
const w_mode = node.widgets?.find(w => w.name === "mode");
const w_prefix = node.widgets?.find(w => w.name === "filename_prefix");
const w_text = node.widgets?.find(w => w.name === "text_input");
const w_image = node.widgets?.find(w => w.name === "image");
const syncWidget = (widget, dataKey) => {
if (widget) {
widget.value = node.data[dataKey];
widget.serializeValue = () => node.data[dataKey];
}
};
if (node.widgets) {
node.widgets.forEach(w => {
if (w.name === "mode" && w.value) node.data.mode = w.value;
if (w.name === "filename_prefix" && w.value) node.data.prefix = w.value;
if (w.name === "text_input" && w.value) node.data.text = w.value;
if (w.name === "image" && w.value) node.data.selected_image = w.value;
w.hidden = true;
w.type = "hidden";
w.computeSize = () => [0, 0];
w.computedHeight = 0;
w.draw = function () {};
if (w.element && w.element.parentNode) {
w.element.parentNode.removeChild(w.element);
}
w.serializeValue = () => {
if (w.name === "mode") return node.data.mode;
if (w.name === "filename_prefix") return node.data.prefix;
if (w.name === "text_input") return node.data.text;
if (w.name === "image") return node.data.selected_image;
return w.value;
};
});
}
node.syncData = function () {
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
node.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
node.imageLoaded = false;
node.imageLoading = false;
const mainContainer = document.createElement("div");
mainContainer.style.cssText = `
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box;
pointer-events: none;
`;
const modeContainer = document.createElement("div");
modeContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;";
const createModeBtn = (label, value) => {
const btn = document.createElement("button");
btn.textContent = label;
btn.style.cssText = `flex:1; height:28px; padding:0 12px; border:2px solid #555; border-radius:4px; font-weight:normal; cursor:pointer; transition:all 0.2s; font-size:12px; pointer-events: auto;`;
btn.onclick = () => {
node.data.mode = value;
syncWidget(w_mode, 'mode');
updateModeVisuals();
node.syncData();
};
return btn;
};
const btnRead = createModeBtn("📄 READ from IMAGE", "read");
const btnWrite = createModeBtn("✏️ WRITE to IMAGE", "write");
modeContainer.appendChild(btnRead);
modeContainer.appendChild(btnWrite);
const updateModeVisuals = () => {
const isRead = node.data.mode === "read";
btnRead.style.backgroundColor = isRead ? "#4CAF50" : "#333";
btnRead.style.borderColor = isRead ? "#81C784" : "#555";
btnRead.style.color = isRead ? "#fff" : "#aaa";
btnWrite.style.backgroundColor = !isRead ? "#2196F3" : "#333";
btnWrite.style.borderColor = !isRead ? "#64B5F6" : "#555";
btnWrite.style.color = !isRead ? "#fff" : "#aaa";
};
updateModeVisuals();
const prefixContainer = document.createElement("div");
prefixContainer.style.cssText = "display:flex; align-items:center; gap:8px; width:100%; pointer-events: auto;";
const prefixLabel = document.createElement("label");
prefixLabel.textContent = "PREFIX:";
prefixLabel.style.cssText = "color:#aaa; font-size:14px; font-weight:normal; white-space:nowrap; pointer-events: auto; cursor:default;";
const prefixInput = document.createElement("input");
prefixInput.type = "text";
prefixInput.value = node.data.prefix;
prefixInput.style.cssText = "flex:1; height:28px; padding:0 12px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; font-size:14px; box-sizing:border-box; pointer-events: auto;";
prefixInput.oninput = () => {
node.data.prefix = prefixInput.value;
syncWidget(w_prefix, 'prefix');
node.syncData();
};
prefixContainer.appendChild(prefixLabel);
prefixContainer.appendChild(prefixInput);
const textContainer = document.createElement("div");
textContainer.style.cssText = "width:100%; pointer-events: auto;";
const customTextArea = document.createElement("textarea");
customTextArea.value = node.data.text;
customTextArea.placeholder = "Enter text here...";
customTextArea.style.cssText = "width:100%; min-height:50px; padding:6px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; resize: none; font-family:monospace; font-size:12px; box-sizing:border-box; pointer-events: auto;";
customTextArea.oninput = () => {
node.data.text = customTextArea.value;
syncWidget(w_text, 'text');
node.syncData();
};
textContainer.appendChild(customTextArea);
const buttonsContainer = document.createElement("div");
buttonsContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;";
const btnUpload = document.createElement("button");
btnUpload.textContent = "📂 UPLOAD IMAGE";
btnUpload.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#4CAF50; border:2px solid #4CAF50; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;";
btnUpload.onmouseover = () => { btnUpload.style.backgroundColor = "#3a3a3a"; };
btnUpload.onmouseout = () => { btnUpload.style.backgroundColor = "#2a2a2a"; };
btnUpload.onclick = () => node.triggerFileUpload();
const btnClear = document.createElement("button");
btnClear.textContent = "🗑️ CLEAR TEXT";
btnClear.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#dc3545; border:2px solid #dc3545; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;";
btnClear.onmouseover = () => { btnClear.style.backgroundColor = "#3a3a3a"; };
btnClear.onmouseout = () => { btnClear.style.backgroundColor = "#2a2a2a"; };
btnClear.onclick = () => {
node.data.text = "";
customTextArea.value = "";
syncWidget(w_text, 'text');
node.syncData();
};
buttonsContainer.appendChild(btnUpload);
buttonsContainer.appendChild(btnClear);
const previewWrap = document.createElement("div");
previewWrap.style.cssText = `
width: 100%;
height: ${PREVIEW_FIXED_HEIGHT}px;
background: #353535;
border: 1px dashed #555;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
pointer-events: auto;
cursor: pointer;
box-sizing: border-box;
flex-shrink: 0;
`;
const placeholder = document.createElement("div");
placeholder.textContent = "📁 Drop Image Here or Click to Upload";
placeholder.style.cssText = "color:#888; font-size:14px; font-family:Arial,sans-serif; text-align:center; pointer-events:none; user-select:none;";
previewWrap.appendChild(placeholder);
const previewImg = document.createElement("img");
previewImg.style.cssText = "display:none; width:100%; height:100%; object-fit:contain; pointer-events:none; user-select:none;";
previewImg.alt = "Image preview";
previewWrap.appendChild(previewImg);
mainContainer.appendChild(modeContainer);
mainContainer.appendChild(prefixContainer);
mainContainer.appendChild(textContainer);
mainContainer.appendChild(buttonsContainer);
mainContainer.appendChild(previewWrap);
node.addDOMWidget("custom_widgets", "customtext", mainContainer);
node.customTextArea = customTextArea;
const buildViewUrl = (imagePath) => {
let filename = imagePath;
let subfolder = "";
if (imagePath.includes("/")) {
const parts = imagePath.split("/");
subfolder = parts[0];
filename = parts.slice(1).join("/");
}
let url = `/view?filename=${encodeURIComponent(filename)}&type=input`;
if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;
return url + `&t=${Date.now()}`;
};
let promptRequestId = 0;
const fetchTextForRead = async (imagePath) => {
if (node.data.mode !== "read" || !imagePath) return;
const requestId = ++promptRequestId;
customTextArea.placeholder = "⏳ Reading text from image...";
try {
const response = await fetch(`/rayko/get_prompt?filename=${encodeURIComponent(imagePath)}`);
if (requestId !== promptRequestId) return;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (requestId !== promptRequestId) return;
const text = result.prompt || "";
node.data.text = text;
customTextArea.value = text;
customTextArea.placeholder = text ? "" : "⚠ No text found in this image";
syncWidget(w_text, 'text');
node.syncData();
} catch (err) {
if (requestId !== promptRequestId) return;
customTextArea.placeholder = "⚠ Failed to read text from image";
console.error("[RS Image-Text] Fetch text error:", err);
}
};
node.loadImage = function (imagePath) {
if (!imagePath || node.imageLoading) return;
node.imageLoading = true;
node.data.selected_image = imagePath;
syncWidget(w_image, 'selected_image');
previewImg.onload = () => {
node.imageLoading = false;
node.imageLoaded = true;
previewImg.style.display = "block";
placeholder.style.display = "none";
node.syncData();
fetchTextForRead(imagePath);
};
previewImg.onerror = () => {
node.imageLoading = false;
node.imageLoaded = false;
previewImg.style.display = "none";
placeholder.style.display = "block";
node.syncData();
};
previewImg.src = buildViewUrl(imagePath);
};
const uploadFileAndLoad = async (file) => {
if (!file || !file.type.startsWith('image/')) return false;
const formData = new FormData();
formData.append('image', file);
formData.append('subfolder', '');
formData.append('type', 'input');
try {
const response = await fetch('/upload/image', { method: 'POST', body: formData });
if (response.ok) {
const result = await response.json();
const imageName = result.name || result.filename;
const subfolder = result.subfolder || '';
const finalName = subfolder ? `${subfolder}/${imageName}` : imageName;
node.loadImage(finalName);
return true;
}
} catch (err) {
console.error("[RS Image-Text] Upload error:", err);
}
return false;
};
node.triggerFileUpload = function () {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/png, image/jpeg, image/webp, image/bmp';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
await uploadFileAndLoad(file);
fileInput.remove();
};
fileInput.click();
};
const onDocumentDragOver = (e) => {
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
e.preventDefault();
}
};
const onDocumentDrop = (e) => {
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
e.preventDefault();
}
};
document.addEventListener('dragover', onDocumentDragOver);
document.addEventListener('drop', onDocumentDrop);
node._docDragOver = onDocumentDragOver;
node._docDrop = onDocumentDrop;
previewWrap.addEventListener('dragover', (e) => {
if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return;
e.preventDefault();
e.stopPropagation();
previewWrap.style.borderColor = '#4CAF50';
previewWrap.style.backgroundColor = '#2a2a2a';
});
previewWrap.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
previewWrap.style.borderColor = '#555';
previewWrap.style.backgroundColor = '#353535';
});
previewWrap.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
previewWrap.style.borderColor = '#555';
previewWrap.style.backgroundColor = '#353535';
const file = e.dataTransfer.files[0];
if (file) {
uploadFileAndLoad(file);
}
});
previewWrap.addEventListener('click', () => {
node.triggerFileUpload();
});
node.onSerialize = function (o) {
syncWidget(w_mode, 'mode');
syncWidget(w_prefix, 'prefix');
syncWidget(w_text, 'text');
syncWidget(w_image, 'selected_image');
o.data = node.data;
};
node.onConfigure = function (o) {
if (o && o.data) {
node.data = { ...node.data, ...o.data };
}
node.imageLoading = false;
node.imageLoaded = false;
if (node.data.selected_image) {
node.loadImage(node.data.selected_image);
}
if (prefixInput) prefixInput.value = node.data.prefix || "";
if (customTextArea) customTextArea.value = node.data.text || "";
updateModeVisuals();
syncWidget(w_mode, 'mode');
syncWidget(w_prefix, 'prefix');
syncWidget(w_text, 'text');
syncWidget(w_image, 'selected_image');
requestAnimationFrame(() => {
if (node.size) node.onResize([...node.size]);
});
};
node.visibilityHandler = function () {
if (!document.hidden && node.graph) {
node.graph.setDirtyCanvas(true, true);
node.syncData();
if (node.size) node.onResize([...node.size]);
}
};
document.addEventListener("visibilitychange", node.visibilityHandler);
const originalOnResize = node.onResize;
node.onResize = function (size) {
if (size[0] < MIN_WIDTH) size[0] = MIN_WIDTH;
if (size[1] < MIN_HEIGHT) size[1] = MIN_HEIGHT;
const titleBarHeight = 30;
const topPadding = 18;
const gap = 8;
const bottomPadding = 5;
const previewSpacing = 35;
const fixedElements = titleBarHeight + topPadding +
28 + gap + 28 + gap +
gap + 28 +
previewSpacing +
PREVIEW_FIXED_HEIGHT +
bottomPadding;
const textareaHeight = Math.max(50, size[1] - fixedElements);
if (customTextArea) {
customTextArea.style.height = textareaHeight + 'px';
}
if (originalOnResize) originalOnResize.apply(this, arguments);
node.setDirtyCanvas(true, true);
};
const originalOnRemoved = node.onRemoved;
node.onRemoved = function () {
document.removeEventListener("visibilitychange", node.visibilityHandler);
if (node._docDragOver) document.removeEventListener('dragover', node._docDragOver);
if (node._docDrop) document.removeEventListener('drop', node._docDrop);
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
};
node.onDrawBackground = function (ctx) {
const w = this.size[0];
const h = this.size[1];
const radius = 8;
ctx.beginPath();
ctx.moveTo(radius, 0);
ctx.lineTo(w - radius, 0);
ctx.quadraticCurveTo(w, 0, w, radius);
ctx.lineTo(w, h - radius);
ctx.quadraticCurveTo(w, h, w - radius, h);
ctx.lineTo(radius, h);
ctx.quadraticCurveTo(0, h, 0, h - radius);
ctx.lineTo(0, radius);
ctx.quadraticCurveTo(0, 0, radius, 0);
ctx.closePath();
ctx.fillStyle = "#353535";
ctx.fill();
};
node.onDrawForeground = function () {};
node.onMouseMove = function () {};
node.onMouseDown = function () { return false; };
if (node.data.selected_image) {
node.loadImage(node.data.selected_image);
}
requestAnimationFrame(() => {
node.onResize([MIN_WIDTH, MIN_HEIGHT]);
});
return result;
};
},
setup() {}
});