Files

572 lines
26 KiB
JavaScript

import { app } from "../../scripts/app.js";
app.registerExtension({
name: "RaykoStudio.LoadImagesFromDir",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RSLoadImagesFromDir") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
node.data = {
folder_path: "",
filter_type: "*.png",
start_index: 1,
end_index: 1,
custom_filter: "*.png"
};
const filters = ["*.*", "*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp", "*.gif", "custom"];
const NODE_WIDTH = 260;
node.targetWidth = NODE_WIDTH;
const MIN_HEIGHT_NORMAL = 255;
const MIN_HEIGHT_CUSTOM = 280;
node.setSize([NODE_WIDTH, MIN_HEIGHT_NORMAL]);
node.min_size = [NODE_WIDTH, MIN_HEIGHT_NORMAL];
const wNodeData = node.widgets?.find(w => w.name === "node_data");
const wPath = node.widgets?.find(w => w.name === "folder_path");
const wFilter = node.widgets?.find(w => w.name === "filter_type");
const wStart = node.widgets?.find(w => w.name === "start_index");
const wEnd = node.widgets?.find(w => w.name === "end_index");
const wCustom = node.widgets?.find(w => w.name === "custom_filter");
[wNodeData, wPath, wFilter, wStart, wEnd, wCustom].forEach(w => {
if (w) {
w.hidden = true;
if (w.element) w.element.style.display = "none";
}
});
if (wNodeData) {
try {
const savedData = JSON.parse(wNodeData.value || "{}");
if (savedData && typeof savedData === 'object') {
node.data = { ...node.data, ...savedData };
}
} catch (e) {}
wNodeData.serializeValue = () => {
node.syncData();
return JSON.stringify(node.data);
};
}
node.uiElements = [];
node.dropdownMenu = null;
node.customModal = null;
node.syncData = function() {
if (wPath) wPath.value = node.data.folder_path;
if (wFilter) wFilter.value = node.data.filter_type;
if (wStart) wStart.value = parseInt(node.data.start_index);
if (wEnd) wEnd.value = parseInt(node.data.end_index);
if (wCustom) wCustom.value = node.data.custom_filter;
if (wNodeData) {
wNodeData.value = JSON.stringify(node.data);
}
if (node.graph) {
node.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
const updateNodeSize = () => {
const targetHeight = node.data.filter_type === "custom" ? MIN_HEIGHT_CUSTOM : MIN_HEIGHT_NORMAL;
node.min_size = [NODE_WIDTH, targetHeight];
if (Math.abs(node.size[1] - targetHeight) > 1) {
node.setSize([node.size[0], targetHeight]);
node.setDirtyCanvas(true, true);
}
};
node.onResize = function() {
const minHeight = this.data.filter_type === "custom" ? MIN_HEIGHT_CUSTOM : MIN_HEIGHT_NORMAL;
if (this.size[0] < NODE_WIDTH) {
this.size[0] = NODE_WIDTH;
}
if (this.size[1] < minHeight) {
this.size[1] = minHeight;
}
this.setDirtyCanvas(true, true);
};
const onConfigure = node.onConfigure;
node.onConfigure = function(o) {
if (onConfigure) onConfigure.apply(this, arguments);
if (wNodeData && wNodeData.value) {
try {
const restoredData = JSON.parse(wNodeData.value);
if (restoredData) {
node.data = { ...node.data, ...restoredData };
node.syncData();
updateNodeSize();
}
} catch (e) {}
}
};
const closeDropdown = (e) => {
if (node.dropdownMenu && !node.dropdownMenu.contains(e.target)) {
node.dropdownMenu.remove();
node.dropdownMenu = null;
}
};
const showCustomPrompt = (title, defaultValue, callback) => {
if (node.customModal) {
node.customModal.remove();
node.customModal = null;
}
const backdrop = document.createElement("div");
backdrop.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5); z-index: 10002;
display: flex; align-items: center; justify-content: center;
`;
const modal = document.createElement("div");
modal.style.cssText = `
background: #1a1a1a; border: 1px solid #444; border-radius: 3px;
padding: 16px; min-width: 320px; max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
font-family: sans-serif; font-size: 12px; color: #ddd;
`;
const titleEl = document.createElement("div");
titleEl.textContent = title;
titleEl.style.cssText = `
margin-bottom: 12px; font-size: 13px; color: #fff; font-weight: bold;
`;
modal.appendChild(titleEl);
const input = document.createElement("input");
input.type = "text";
input.value = defaultValue || "";
input.style.cssText = `
width: 100%; box-sizing: border-box; padding: 8px 10px;
background: #222; border: 1px solid #666; border-radius: 3px;
color: #fff; font-size: 12px; outline: none;
font-family: monospace;
`;
input.onfocus = () => { input.style.borderColor = "#2196F3"; };
input.onblur = () => { input.style.borderColor = "#666"; };
modal.appendChild(input);
const btnContainer = document.createElement("div");
btnContainer.style.cssText = `
display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px;
`;
const btnPaste = document.createElement("div");
btnPaste.setAttribute("role", "button");
btnPaste.setAttribute("tabindex", "0");
btnPaste.textContent = " Paste";
btnPaste.style.cssText = `
padding: 6px 16px; background: #2a2a2a; border: 1px solid #888;
border-radius: 3px; color: #ddd; font-size: 12px; cursor: pointer;
font-family: sans-serif; min-width: 80px; transition: all 0.2s;
user-select: none; -moz-user-select: none;
display: inline-flex; align-items: center; justify-content: center;
`;
btnPaste.onmousedown = (e) => e.preventDefault();
btnPaste.onmouseup = (e) => e.preventDefault();
btnPaste.oncontextmenu = (e) => e.preventDefault();
btnPaste.onmouseover = () => { if (!btnPaste.disabled) btnPaste.style.background = "#3a3a3a"; };
btnPaste.onmouseout = () => { if (!btnPaste.disabled) btnPaste.style.background = "#2a2a2a"; };
btnPaste.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
btnPaste.disabled = true;
btnPaste.style.opacity = "0.7";
btnPaste.style.cursor = "wait";
btnPaste.textContent = "⏳ ...";
try {
const text = await navigator.clipboard.readText();
if (text) {
input.value = text;
input.focus();
btnPaste.textContent = "✅ Done";
btnPaste.style.borderColor = "#4CAF50";
btnPaste.style.color = "#4CAF50";
}
} catch (err) {
btnPaste.textContent = "⌨️ Ctrl+V";
btnPaste.style.borderColor = "#FF9800";
btnPaste.style.color = "#FF9800";
input.focus();
input.select();
}
setTimeout(() => {
btnPaste.disabled = false;
btnPaste.style.opacity = "1";
btnPaste.style.cursor = "pointer";
btnPaste.textContent = " Paste";
btnPaste.style.borderColor = "#888";
btnPaste.style.color = "#ddd";
btnPaste.style.background = "#2a2a2a";
}, 600);
};
const btnCancel = document.createElement("button");
btnCancel.textContent = "Cancel";
btnCancel.style.cssText = `
padding: 6px 16px; background: #2a2a2a; border: 1px solid #888;
border-radius: 3px; color: #ddd; font-size: 12px; cursor: pointer;
font-family: sans-serif;
`;
btnCancel.onmouseover = () => { btnCancel.style.background = "#3a3a3a"; };
btnCancel.onmouseout = () => { btnCancel.style.background = "#2a2a2a"; };
const btnOk = document.createElement("button");
btnOk.textContent = "OK";
btnOk.style.cssText = `
padding: 6px 16px; background: #2196F3; border: 1px solid #2196F3;
border-radius: 3px; color: #fff; font-size: 12px; cursor: pointer;
font-family: sans-serif; font-weight: bold;
`;
btnOk.onmouseover = () => { btnOk.style.background = "#1976D2"; };
btnOk.onmouseout = () => { btnOk.style.background = "#2196F3"; };
btnContainer.appendChild(btnPaste);
btnContainer.appendChild(btnCancel);
btnContainer.appendChild(btnOk);
modal.appendChild(btnContainer);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
node.customModal = backdrop;
const close = (result) => {
backdrop.remove();
node.customModal = null;
document.removeEventListener("keydown", keyHandler);
if (callback) callback(result);
};
const confirm = () => close(input.value);
const cancel = () => close(null);
btnOk.onclick = (e) => { e.stopPropagation(); confirm(); };
btnCancel.onclick = (e) => { e.stopPropagation(); cancel(); };
backdrop.onclick = (e) => {
if (e.target === backdrop) cancel();
};
const keyHandler = (e) => {
if (e.key === "Enter") {
e.preventDefault();
confirm();
} else if (e.key === "Escape") {
e.preventDefault();
cancel();
}
};
document.addEventListener("keydown", keyHandler);
setTimeout(() => {
input.focus();
input.select();
}, 10);
};
const truncatePath = (ctx, path, maxWidth) => {
if (!path) return "Folder not selected";
const ellipsis = "...";
if (ctx.measureText(path).width <= maxWidth) {
return path;
}
let truncated = path;
while (truncated.length > 0 && ctx.measureText(ellipsis + truncated).width > maxWidth) {
truncated = truncated.substring(1);
}
return ellipsis + truncated;
};
const drawRoundedRect = (ctx, x, y, w, h, r) => {
if (ctx.roundRect) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
ctx.fill();
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
};
node.onDrawForeground = function(ctx) {
if (this.flags.collapsed) return;
ctx.save(); // ← ДОБАВЛЕНО
try {
const [w, h] = this.size;
const padding = 10;
const rowHeight = 23;
const borderRadius = 3;
let currentY = 35 + 60;
ctx.font = "12px sans-serif";
ctx.textBaseline = "middle";
const pathDisplayHeight = 20;
ctx.fillStyle = "#1a1a1a";
ctx.strokeStyle = "#333";
drawRoundedRect(ctx, padding, currentY, w - padding * 2, pathDisplayHeight, borderRadius);
const pathText = node.data.folder_path || "Folder not selected";
const displayPath = truncatePath(ctx, pathText, w - padding * 2 - 10);
ctx.fillStyle = node.data.folder_path ? "#aaa" : "#666";
ctx.textAlign = "left";
ctx.fillText(displayPath, padding + 5, currentY + pathDisplayHeight / 2);
currentY += pathDisplayHeight + 7;
ctx.fillStyle = "#2a2a2a";
ctx.strokeStyle = "#2196F3";
drawRoundedRect(ctx, padding, currentY, w - padding * 2, rowHeight, borderRadius);
ctx.fillStyle = "#2196F3";
ctx.textAlign = "center";
ctx.fillText("📁 Select folder", w / 2, currentY + rowHeight / 2);
this.uiElements.push({ type: "btn_folder", x: padding, y: currentY, w: w - padding * 2, h: rowHeight });
currentY += rowHeight + 7;
const filterLabel = `▼ ${node.data.filter_type}`;
ctx.fillStyle = "#2a2a2a";
ctx.strokeStyle = "#4CAF50";
drawRoundedRect(ctx, padding, currentY, w - padding * 2, rowHeight, borderRadius);
ctx.fillStyle = "#4CAF50";
ctx.textAlign = "center";
ctx.fillText(filterLabel, w / 2, currentY + rowHeight / 2);
this.uiElements.push({ type: "btn_filter", x: padding, y: currentY, w: w - padding * 2, h: rowHeight });
currentY += rowHeight + 7;
const drawNumberInput = (label, value, yPos, dataType) => {
const labelW = 40;
const btnW = 20;
const gap = 4;
const totalW = w - padding * 2;
const valW = totalW - labelW - (btnW * 2) - (gap * 2);
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.fillText(label, padding, yPos + rowHeight / 2);
const btnX1 = padding + labelW + gap;
ctx.fillStyle = "#333";
ctx.strokeStyle = "#888";
drawRoundedRect(ctx, btnX1, yPos, btnW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText("-", btnX1 + btnW / 2, yPos + rowHeight / 2);
const valX = btnX1 + btnW + gap;
ctx.fillStyle = "#222";
ctx.strokeStyle = "#666";
drawRoundedRect(ctx, valX, yPos, valW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText(String(value), valX + valW / 2, yPos + rowHeight / 2);
const btnX2 = valX + valW + gap;
ctx.fillStyle = "#333";
ctx.strokeStyle = "#888";
drawRoundedRect(ctx, btnX2, yPos, btnW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText("+", btnX2 + btnW / 2, yPos + rowHeight / 2);
this.uiElements.push({ type: `btn_${dataType}_minus`, x: btnX1, y: yPos, w: btnW, h: rowHeight });
this.uiElements.push({ type: `val_${dataType}`, x: valX, y: yPos, w: valW, h: rowHeight });
this.uiElements.push({ type: `btn_${dataType}_plus`, x: btnX2, y: yPos, w: btnW, h: rowHeight });
};
drawNumberInput("Start:", node.data.start_index, currentY, "start");
currentY += rowHeight + 5;
drawNumberInput("End:", node.data.end_index, currentY, "end");
currentY += rowHeight + 12;
if (node.data.filter_type === "custom") {
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.fillText("Custom:", padding, currentY + rowHeight / 2);
const custValW = w - padding * 2 - 45;
const custX = padding + 45;
ctx.fillStyle = "#222";
ctx.strokeStyle = "#FF9800";
drawRoundedRect(ctx, custX, currentY, custValW, rowHeight, borderRadius);
ctx.fillStyle = "#FF9800";
ctx.textAlign = "left";
ctx.fillText(node.data.custom_filter, custX + 5, currentY + rowHeight / 2);
this.uiElements.push({ type: "val_custom", x: custX, y: currentY, w: custValW, h: rowHeight });
}
} finally {
ctx.restore(); // ← ДОБАВЛЕНО
}
};
node.onMouseDown = function(event, pos) {
const [x, y] = pos;
this.uiElements = [];
this.onDrawForeground(app.canvas.ctx);
for (const el of this.uiElements) {
if (x >= el.x && x <= el.x + el.w && y >= el.y && y <= el.y + el.h) {
if (el.type === "btn_folder") {
showCustomPrompt("Enter folder path", this.data.folder_path, (result) => {
if (result !== null) {
this.data.folder_path = result.trim();
node.syncData();
}
});
return true;
}
if (el.type === "btn_filter") {
if (this.dropdownMenu) this.dropdownMenu.remove();
const menu = document.createElement("div");
menu.style.cssText = `
position: fixed; background: #1a1a1a; border: 1px solid #444;
border-radius: 3px; max-height: 255px; overflow-y: auto;
z-index: 10001; box-shadow: 0 4px 20px rgba(0,0,0,0.5); min-width: 150px;
`;
filters.forEach(f => {
const item = document.createElement("div");
item.textContent = f;
item.style.cssText = `
padding: 8px 12px; cursor: pointer; color: #ddd; font-size: 12px;
border-bottom: 1px solid #333;
background: ${f === this.data.filter_type ? '#333' : '#1a1a1a'};
`;
item.onmouseover = () => item.style.background = "#444";
item.onmouseout = () => item.style.background = f === this.data.filter_type ? '#333' : "#1a1a1a";
item.onclick = (e) => {
e.stopPropagation();
this.data.filter_type = f;
node.syncData();
updateNodeSize();
menu.remove();
this.dropdownMenu = null;
};
menu.appendChild(item);
});
const canvasRect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale);
const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale);
menu.style.left = (nodeScreenX + el.x) + "px";
menu.style.top = (nodeScreenY + el.y + el.h + 5) + "px";
document.body.appendChild(menu);
this.dropdownMenu = menu;
setTimeout(() => document.addEventListener("mousedown", closeDropdown), 100);
return true;
}
if (el.type === "btn_start_minus") {
this.data.start_index = Math.max(1, parseInt(this.data.start_index) - 1);
node.syncData();
return true;
}
if (el.type === "btn_start_plus") {
this.data.start_index = parseInt(this.data.start_index) + 1;
node.syncData();
return true;
}
if (el.type === "val_start") {
showCustomPrompt("Start index (min 1)", String(this.data.start_index), (result) => {
if (result !== null) {
this.data.start_index = Math.max(1, parseInt(result) || 1);
node.syncData();
}
});
return true;
}
if (el.type === "btn_end_minus") {
this.data.end_index = Math.max(1, parseInt(this.data.end_index) - 1);
node.syncData();
return true;
}
if (el.type === "btn_end_plus") {
this.data.end_index = parseInt(this.data.end_index) + 1;
node.syncData();
return true;
}
if (el.type === "val_end") {
showCustomPrompt("End index", String(this.data.end_index), (result) => {
if (result !== null) {
this.data.end_index = Math.max(1, parseInt(result) || 1);
node.syncData();
}
});
return true;
}
if (el.type === "val_custom") {
showCustomPrompt("Custom filter (e.g. *_mask.png)", this.data.custom_filter, (result) => {
if (result !== null) {
this.data.custom_filter = result.trim() || "*.png";
node.syncData();
}
});
return true;
}
}
}
return false;
};
const originalOnRemoved = node.onRemoved;
node.onRemoved = function() {
if (this.dropdownMenu) {
this.dropdownMenu.remove();
this.dropdownMenu = null;
}
if (this.customModal) {
this.customModal.remove();
this.customModal = null;
}
document.removeEventListener("mousedown", closeDropdown);
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
};
node.syncData();
updateNodeSize();
return result;
};
}
});