Files
Raykosan-ComfyUI_RaykoStudio/web/rs_styles.js
T
2026-07-09 21:33:12 +03:00

603 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "RaykoStylesLoader",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "Rayko_Styles_CSV_Loader") {
console.log("[Rayko] RS Styles Loader JS loaded");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.data = {
active_csv_file: "",
styles: [],
csv_list: []
};
this.rowHeight = 28;
this.padding = 10;
this.labelWidth = 110;
this.targetWidth = 340;
this.clickZones = [];
this.menuPosition = null;
this.menuScrollPosition = 0;
const self = this;
this.hiddenWidget = this.widgets.find(w => w.name === "node_data");
if (this.hiddenWidget) {
this.hiddenWidget.hidden = true;
this.hiddenWidget.tooltip = "";
this.hiddenWidget.type = "hidden";
if (this.hiddenWidget.element) {
this.hiddenWidget.element.style.display = "none";
this.hiddenWidget.element.style.pointerEvents = "none";
}
try {
const savedData = JSON.parse(this.hiddenWidget.value || "{}");
if (savedData && typeof savedData === 'object') {
this.data = {
...this.data,
...savedData,
csv_list: this.data.csv_list.length > 0 ? this.data.csv_list : savedData.csv_list || []
};
}
} catch (e) {
console.error("[Rayko] Error loading saved data", e);
}
this.hiddenWidget.serializeValue = () => {
this.syncData();
return JSON.stringify(this.data);
};
}
if (this.widgets) {
this.widgets.forEach(w => {
if (w.name !== "node_data") {
w.hidden = true;
}
});
}
this.setSize([this.targetWidth, 300]);
this.loadCSVList = async function() {
try {
const response = await api.fetchApi("/rayko_get_csv_files");
if (response.ok) {
const files = await response.json();
self.data.csv_list = files.length > 0 ? files : ["No CSV files"];
if (!self.data.active_csv_file || self.data.active_csv_file === "No CSV files" || !self.data.csv_list.includes(self.data.active_csv_file)) {
self.data.active_csv_file = self.data.csv_list[0];
}
self.syncData();
self.graph?.setDirtyCanvas(true, true);
}
} catch (e) {
console.error("[Rayko] Error loading CSV list:", e);
self.data.csv_list = ["No CSV files"];
}
};
this.drawSeparator = function(ctx, text, x, y, w, h) {
ctx.fillStyle = "#444";
ctx.fillRect(x, y + 8, w, 1);
ctx.fillStyle = "#888";
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.fillText(text, x + w/2, y + 22);
};
this.drawLabel = function(ctx, text, x, y, w, h) {
ctx.fillStyle = "#aaa";
ctx.font = "11px sans-serif";
ctx.textAlign = "left";
ctx.fillText(text, x, y + h/2 + 4);
};
this.drawComboField = function(ctx, value, x, y, w, h) {
ctx.fillStyle = "#222";
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = "#444";
ctx.strokeRect(x, y, w, h);
ctx.fillStyle = "#fff";
ctx.font = "11px sans-serif";
ctx.textAlign = "center";
let displayValue = value || "No CSV files";
if (displayValue.length > 25) {
displayValue = displayValue.substring(0, 22) + "...";
}
ctx.fillText(displayValue, x + w/2, y + h/2 + 4);
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.moveTo(x + w - 12, y + h/2 - 3);
ctx.lineTo(x + w - 6, y + h/2 - 3);
ctx.lineTo(x + w - 9, y + h/2 + 3);
ctx.fill();
};
this.drawButton = function(ctx, text, x, y, w, h, iconColor = "#fff") {
ctx.fillStyle = "#242427";
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = "#3a3a3c";
ctx.strokeRect(x, y, w, h);
ctx.font = "bold 11px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const parts = text.split(" ");
const icon = parts[0];
const label = parts.slice(1).join(" ");
ctx.fillStyle = iconColor;
ctx.fillText(icon, x + w/2 - (label.length * 3.5), y + h/2);
ctx.fillStyle = "#fff";
ctx.fillText(label, x + w/2 + 8, y + h/2);
};
this.onDrawForeground = function(ctx, visibleRect) {
this.clickZones = [];
const startY = 80;
const rowH = self.rowHeight;
const pad = self.padding;
let y = startY;
self.drawLabel(ctx, "SELECT CSV FILE", pad, y, self.labelWidth, rowH);
self.drawComboField(ctx, self.data.active_csv_file, pad + self.labelWidth, y, self.size[0] - pad*2 - self.labelWidth, rowH);
this.clickZones.push({ type: "combo", field: "active_csv_file", x: pad + self.labelWidth, y: y, w: self.size[0] - pad*2 - self.labelWidth, h: rowH });
y += rowH + 10;
self.drawButton(ctx, "📂 UPLOAD NEW CSV FILE", pad, y, self.size[0] - pad*2, rowH, "#FF9800");
this.clickZones.push({ type: "upload", x: pad, y: y, w: self.size[0] - pad*2, h: rowH });
y += rowH + 10;
self.drawButton(ctx, "➕ ADD STYLE", pad, y, self.size[0] - pad*2, rowH, "#9C27B0");
this.clickZones.push({ type: "add_style", x: pad, y: y, w: self.size[0] - pad*2, h: rowH });
y += rowH + 15;
self.drawSeparator(ctx, "ACTIVE STYLES", pad, y, self.size[0] - pad*2, rowH);
y += rowH + 10;
const rightPanelWidth = 40;
const toggleStartX = 20;
const toggleRadius = 7;
const nameStartX = 42;
for (let i = 0; i < self.data.styles.length; i++) {
const row = self.data.styles[i];
const styleY = y + (i * rowH);
const h = rowH - 2;
const centerY = styleY + h/2;
ctx.fillStyle = i % 2 === 0 ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.15)";
ctx.fillRect(pad, styleY, self.size[0] - (pad * 2), h);
ctx.fillStyle = row.enabled ? "#4CAF50" : "#555";
ctx.beginPath();
ctx.arc(toggleStartX, centerY, toggleRadius, 0, Math.PI * 2);
ctx.fill();
this.clickZones.push({ type: "toggle", index: i, x: toggleStartX - toggleRadius - 5, y: styleY, w: toggleRadius * 2 + 10, h: h });
const nameW = self.size[0] - nameStartX - rightPanelWidth - pad - 10;
ctx.fillStyle = row.enabled ? "#fff" : "#777";
ctx.font = "12px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
let displayName = row.display_name || row.name;
if (displayName.includes("|")) {
const parts = displayName.split("|");
displayName = parts[parts.length - 1].trim();
}
if (ctx.measureText(displayName).width > nameW) {
while (ctx.measureText(displayName + "...").width > nameW && displayName.length > 0) {
displayName = displayName.slice(0, -1);
}
displayName = displayName + "...";
}
ctx.fillText(displayName, nameStartX, centerY);
this.clickZones.push({ type: "name", index: i, x: nameStartX, y: styleY, w: nameW, h: h });
const delX = self.size[0] - rightPanelWidth;
ctx.fillStyle = "#f44336";
ctx.fillText("❌", delX, centerY);
this.clickZones.push({ type: "delete", index: i, x: delX, y: styleY, w: 30, h: h });
}
const totalH = y + (self.data.styles.length * rowH) + 20;
if (self.size[1] < totalH) {
self.setSize([self.targetWidth, totalH]);
}
};
this.onMouseDown = function(e, pos, canvas) {
if (!this.clickZones.length) return false;
for (const zone of this.clickZones) {
const inX = pos[0] >= zone.x && pos[0] <= zone.x + zone.w;
const inY = pos[1] >= zone.y && pos[1] <= zone.y + zone.h;
if (inX && inY) {
if (zone.type === "combo" && zone.field === "active_csv_file") {
self.showCSVSelector(e);
return true;
}
if (zone.type === "upload") {
self.uploadCSVFile();
return true;
}
if (zone.type === "add_style") {
self.showStyleSelector(e);
return true;
}
if (zone.type === "toggle") {
self.data.styles[zone.index].enabled = !self.data.styles[zone.index].enabled;
self.syncData();
self.graph.setDirtyCanvas(true, true);
self.graph.changeTracker?.dispatchEvent(new Event("change"));
return true;
}
if (zone.type === "delete") {
self.data.styles.splice(zone.index, 1);
self.syncData();
self.updateUI();
return true;
}
}
}
return false;
};
this.showCSVSelector = function(clickEvent) {
const list = self.data.csv_list || [];
if (!list.length) return;
const existingMenu = document.getElementById("rayko-csv-selector");
if (existingMenu) existingMenu.remove();
const menu = document.createElement("div");
menu.id = "rayko-csv-selector";
menu.style.cssText = `position:fixed;background:#1a1a1a;border:1px solid #444;border-radius:6px;max-height:300px;overflow-y:auto;z-index:10001;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:200px;`;
list.forEach(opt => {
const item = document.createElement("div");
item.textContent = opt;
item.style.cssText = `padding:10px 15px;cursor:pointer;color:#ddd;font-size:12px;border-bottom:1px solid #333;background:${opt === self.data.active_csv_file ? '#333' : '#1a1a1a'};`;
item.onmouseover = () => item.style.background = "#444";
item.onmouseout = () => item.style.background = opt === self.data.active_csv_file ? '#333' : "#1a1a1a";
item.onclick = (ev) => {
ev.stopPropagation();
self.data.active_csv_file = opt;
self.syncData();
self.updateUI();
closeMenu();
};
menu.appendChild(item);
});
if (clickEvent) {
menu.style.left = (clickEvent.clientX + 8) + "px";
menu.style.top = clickEvent.clientY + "px";
}
document.body.appendChild(menu);
const closeMenu = () => {
menu.remove();
document.removeEventListener("pointerdown", closeOutside, true);
document.removeEventListener("keydown", closeEsc, true);
};
const closeOutside = (ev) => {
if (!menu.contains(ev.target)) closeMenu();
};
const closeEsc = (ev) => {
if (ev.key === "Escape") closeMenu();
};
document.addEventListener("pointerdown", closeOutside, true);
document.addEventListener("keydown", closeEsc, true);
};
this.uploadCSVFile = async function() {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".csv";
fileInput.style.display = "none";
fileInput.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (file) {
try {
const formData = new FormData();
formData.append("file", file);
const response = await api.fetchApi("/rayko_upload_csv_file", { method: "POST", body: formData });
if (response.ok) {
const data = await response.json();
await self.loadCSVList();
self.data.active_csv_file = data.filename;
self.syncData();
self.graph.setDirtyCanvas(true, true);
self.graph.changeTracker?.dispatchEvent(new Event("change"));
} else {
const errorText = await response.text();
alert("Failed: " + errorText);
}
} catch (error) {
alert("Error: " + error.message);
}
}
if (fileInput.parentNode) fileInput.parentNode.removeChild(fileInput);
});
document.body.appendChild(fileInput);
fileInput.click();
};
this.showStyleSelector = async function(clickEvent) {
if (!self.data.active_csv_file || self.data.active_csv_file === "No CSV files") {
alert("⚠️ Upload CSV first!");
return;
}
try {
const response = await api.fetchApi("/rayko_get_styles_from_file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: self.data.active_csv_file })
});
if (response.ok) {
const data = await response.json();
const allStyles = data.styles || [];
if (allStyles.length === 0) {
alert("⚠️ No styles in CSV!");
return;
}
const tree = { _folders: [], _styles: [] };
let currentFolder = null;
for (const style of allStyles) {
if (!style) continue;
if (style.startsWith("|||")) {
const folderName = style.replace("|||", "").trim();
currentFolder = folderName;
if (!tree[folderName]) {
tree[folderName] = { _folders: [], _styles: [] };
tree._folders.push(folderName);
}
} else if (currentFolder && style.includes("|")) {
const styleFullName = style.trim();
const parts = styleFullName.split("|");
const styleDisplayName = parts[parts.length - 1].trim();
if (tree[currentFolder]) {
tree[currentFolder]._styles.push({ fullName: styleFullName, displayName: styleDisplayName });
}
} else {
tree._styles.push({ fullName: style.trim(), displayName: style.trim() });
}
}
self.menuPosition = {
left: (clickEvent.clientX + 8) + "px",
top: clickEvent.clientY + "px"
};
self.menuScrollPosition = 0;
self.showStyleTreeMenu(tree, {}, self.menuPosition);
}
} catch (e) {
alert("Error: " + e.message);
}
};
this.showStyleTreeMenu = function(tree, expandedFolders, fixedPosition) {
const currentScroll = self.menuScrollPosition || 0;
const existingMenu = document.getElementById("rayko-style-menu");
if (existingMenu) existingMenu.remove();
const menu = document.createElement("div");
menu.id = "rayko-style-menu";
menu.style.cssText = `position:fixed;background:#1a1a1a;border:1px solid #444;border-radius:6px;max-height:400px;overflow-y:auto;z-index:10001;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:250px;`;
const header = document.createElement("div");
header.textContent = `📁 ${self.data.active_csv_file}`;
header.style.cssText = `padding:10px 12px;color:#fff;font-weight:bold;border-bottom:1px solid #333;background:#252525;position:sticky;top:0;`;
menu.appendChild(header);
const contentContainer = document.createElement("div");
contentContainer.id = "rayko-style-content";
menu.appendChild(contentContainer);
const closeMenu = () => {
self.menuPosition = null;
self.menuScrollPosition = 0;
menu.remove();
document.removeEventListener("pointerdown", closeOutside, true);
document.removeEventListener("keydown", closeEsc, true);
};
const closeOutside = (ev) => {
if (!menu.contains(ev.target)) closeMenu();
};
const closeEsc = (ev) => {
if (ev.key === "Escape") closeMenu();
};
function renderContent() {
contentContainer.innerHTML = "";
if (tree._styles && tree._styles.length > 0) {
for (const styleObj of tree._styles) {
const item = document.createElement("div");
item.textContent = "🎨 " + styleObj.displayName;
item.style.cssText = `padding:10px 12px;cursor:pointer;color:#ddd;border-bottom:1px solid #333;font-size:13px;`;
item.style.backgroundColor = "#1a1a1a";
item.onmouseover = () => item.style.backgroundColor = "#333";
item.onmouseout = () => item.style.backgroundColor = "#1a1a1a";
item.onclick = (ev) => { ev.stopPropagation(); self.addStyleRow(styleObj.fullName, styleObj.displayName); closeMenu(); };
contentContainer.appendChild(item);
}
const separator = document.createElement("div");
separator.style.cssText = `height:1px;background:#333;margin:5px 0;`;
contentContainer.appendChild(separator);
}
if (tree._folders && tree._folders.length > 0) {
for (const folderName of tree._folders) {
renderFolderItem(folderName, tree[folderName], contentContainer, 0);
}
}
}
function renderFolderItem(folderName, folderData, container, level) {
const folderPath = folderName;
const isExpanded = expandedFolders[folderPath] || false;
const folderHeader = document.createElement("div");
folderHeader.style.cssText = `padding:10px 12px;cursor:pointer;color:#ffd700;font-weight:bold;border-bottom:1px solid #333;background:${level === 0 ? '#252525' : '#1f1f1f'};font-size:13px;padding-left:${12 + level * 16}px;`;
folderHeader.innerHTML = `<span style="margin-right:8px;">${isExpanded ? "▼" : "▶"}</span> 📁 ${folderName}`;
folderHeader.onclick = (ev) => {
ev.stopPropagation();
const savedScroll = menu.scrollTop;
expandedFolders[folderPath] = !isExpanded;
renderContent();
setTimeout(() => { menu.scrollTop = savedScroll; }, 0);
};
container.appendChild(folderHeader);
if (isExpanded) {
if (folderData._styles && folderData._styles.length > 0) {
for (const styleObj of folderData._styles) {
const styleItem = document.createElement("div");
styleItem.textContent = "🎨 " + styleObj.displayName;
styleItem.style.cssText = `padding:10px 12px;cursor:pointer;color:#ddd;border-bottom:1px solid #333;font-size:13px;padding-left:${12 + (level + 1) * 16}px;`;
styleItem.style.backgroundColor = "#1a1a1a";
styleItem.onmouseover = () => styleItem.style.backgroundColor = "#333";
styleItem.onmouseout = () => styleItem.style.backgroundColor = "#1a1a1a";
styleItem.onclick = (ev) => { ev.stopPropagation(); self.addStyleRow(styleObj.fullName, styleObj.displayName); closeMenu(); };
container.appendChild(styleItem);
}
}
if (folderData._folders && folderData._folders.length > 0) {
for (const subFolderName of folderData._folders) {
renderFolderItem(subFolderName, folderData[subFolderName], container, level + 1);
}
}
}
}
renderContent();
if (fixedPosition) {
menu.style.left = fixedPosition.left;
menu.style.top = fixedPosition.top;
}
setTimeout(() => {
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
menu.style.left = (window.innerWidth - rect.width - 10) + "px";
}
if (rect.bottom > window.innerHeight) {
menu.style.top = (window.innerHeight - rect.height - 10) + "px";
}
if (currentScroll > 0) {
menu.scrollTop = currentScroll;
}
}, 10);
document.body.appendChild(menu);
menu.addEventListener("scroll", () => {
self.menuScrollPosition = menu.scrollTop;
});
document.addEventListener("pointerdown", closeOutside, true);
document.addEventListener("keydown", closeEsc, true);
};
this.addStyleRow = function(styleFullName, styleDisplayName) {
const exists = self.data.styles.some(row => row.name === styleFullName && row.file === self.data.active_csv_file);
if (exists) { alert("⚠️ Already added!"); return; }
self.data.styles.push({ name: styleFullName, display_name: styleDisplayName, file: self.data.active_csv_file, enabled: true });
self.syncData();
self.updateUI();
self.menuPosition = null;
self.menuScrollPosition = 0;
};
this.syncData = function() {
if (this.hiddenWidget) {
this.hiddenWidget.value = JSON.stringify(this.data);
}
if (this.graph) {
this.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
this.updateUI = function() {
self.syncData();
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
self.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
const onSerialize = this.onSerialize;
this.onSerialize = function(o) {
self.syncData();
if (onSerialize) {
return onSerialize.apply(this, arguments);
}
};
const onConfigure = this.onConfigure;
this.onConfigure = function(o) {
if (onConfigure) {
onConfigure.apply(this, arguments);
}
if (this.hiddenWidget && this.hiddenWidget.value) {
try {
const restoredData = JSON.parse(this.hiddenWidget.value);
if (restoredData) {
this.data = { ...this.data, ...restoredData };
if (!this.data.csv_list.length && restoredData.csv_list) {
this.data.csv_list = restoredData.csv_list;
}
this.setSize([this.targetWidth, 300]);
this.graph?.setDirtyCanvas(true, true);
}
} catch (e) {
console.error("[Rayko] Error restoring data:", e);
}
}
};
this.loadCSVList();
return result;
};
}
}
});