Files
Raykosan-ComfyUI_RaykoStudio/web/rs_styles.js
T

1508 lines
76 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: [],
favorites: {}
};
this.rowHeight = 28;
this.toolbarHeight = 28;
this.padding = 10;
this.labelWidth = 110;
this.targetWidth = 340;
this.targetHeight = 300;
this.MIN_WIDTH = 340;
this.MIN_HEIGHT = 340;
this.clickZones = [];
this.menuPosition = null;
this.menuScrollPosition = 0;
this.expandedFolders = {};
this.manual_size = false;
this.scrollOffset = 0;
this.isAutoResizing = false;
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 || [],
favorites: savedData.favorites || {}
};
this.expandedFolders = savedData.expandedFolders || {};
this.manual_size = savedData.manual_size || false;
this.scrollOffset = savedData.scrollOffset || 0;
}
} 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, this.targetHeight]);
this.syncData = function() {
if (this.hiddenWidget) {
this.hiddenWidget.value = JSON.stringify({
...this.data,
expandedFolders: this.expandedFolders,
manual_size: this.manual_size,
scrollOffset: this.scrollOffset
});
}
if (this.graph) {
this.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
this.updateUI = function() {
this.syncData();
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
this.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
this.wheelHandler = function(e) {
if (app.canvas.node_over !== self) {
return;
}
const graphPos = app.canvas.graph_mouse;
if (!graphPos) return;
const relY = graphPos[1] - self.pos[1];
const startY = 80;
const rowH = self.rowHeight;
let y = startY;
y += rowH + 10;
y += rowH + 10;
y += rowH + 10;
y += rowH + 15;
y += rowH + 5;
y += self.toolbarHeight + 8;
const availableHeight = self.size[1] - y - 10;
const maxVisibleStyles = Math.max(1, Math.floor(availableHeight / rowH));
const stylesEndY = y + maxVisibleStyles * rowH;
if (relY < y || relY > stylesEndY) {
return;
}
if (self.data.styles.length <= maxVisibleStyles) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const delta = e.deltaY > 0 ? 1 : -1;
const maxOffset = self.data.styles.length - maxVisibleStyles;
const newOffset = Math.max(0, Math.min(self.scrollOffset + delta, maxOffset));
if (newOffset !== self.scrollOffset) {
self.scrollOffset = newOffset;
self.syncData();
self.graph.setDirtyCanvas(true, true);
}
};
const initialCanvas = app.canvas.canvas;
initialCanvas.addEventListener('wheel', this.wheelHandler, { capture: true, passive: false });
this.visibilityHandler = function() {
if (!document.hidden) {
setTimeout(() => {
const currentCanvas = app.canvas.canvas;
if (self.oldWheelCanvas && self.oldWheelCanvas !== currentCanvas) {
self.oldWheelCanvas.removeEventListener('wheel', self.oldWheelHandler, { capture: true, passive: false });
currentCanvas.addEventListener('wheel', self.wheelHandler, { capture: true, passive: false });
self.oldWheelCanvas = currentCanvas;
}
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
}
self.updateUI();
}, 150);
}
};
document.addEventListener("visibilitychange", this.visibilityHandler);
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.loadFavoritesFromServer = async function() {
try {
const response = await api.fetchApi("/rayko_get_favorites");
if (response.ok) {
const favorites = await response.json();
self.data.favorites = favorites;
return favorites;
}
} catch (e) {
console.error("[Rayko] Error loading favorites:", e);
}
return {};
};
this.saveFavoritesToServer = async function() {
try {
const response = await api.fetchApi("/rayko_save_favorites", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(self.data.favorites)
});
if (!response.ok) {
console.error("[Rayko] Error saving favorites:", await response.text());
}
} catch (e) {
console.error("[Rayko] Error saving favorites:", e);
}
};
this.migrateFavorites = async function() {
const serverFavorites = await self.loadFavoritesFromServer();
const hasServerData = Object.keys(serverFavorites).length > 0;
const hasLocalData = self.data.favorites && Object.keys(self.data.favorites).length > 0;
if (!hasServerData && hasLocalData) {
console.log("[Rayko] Migrating favorites from node_data to server");
await self.saveFavoritesToServer();
} else if (hasServerData) {
self.data.favorites = serverFavorites;
}
};
this.loadPresetsList = async function() {
try {
const response = await api.fetchApi("/rayko_get_presets");
if (response.ok) {
return await response.json();
}
} catch (e) {
console.error("[Rayko] Error loading presets:", e);
}
return [];
};
this.savePreset = async function(name) {
const presetData = {
csv_file: self.data.active_csv_file,
styles: self.data.styles
};
try {
const response = await api.fetchApi("/rayko_save_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, ...presetData })
});
return response.ok;
} catch (e) {
console.error("[Rayko] Error saving preset:", e);
return false;
}
};
this.loadPreset = async function(name) {
try {
const response = await api.fetchApi("/rayko_load_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
});
if (response.ok) {
const data = await response.json();
requestAnimationFrame(() => {
if (data.csv_file) {
self.data.active_csv_file = data.csv_file;
}
if (data.styles) {
self.data.styles = data.styles;
}
self.scrollOffset = 0;
self.manual_size = false;
const startY = 80;
const rowH = self.rowHeight;
let y = startY;
y += rowH + 10;
y += rowH + 10;
y += rowH + 10;
y += rowH + 15;
y += rowH + 5;
y += self.toolbarHeight + 8;
const desiredVisible = Math.min(self.data.styles.length, 10);
const calculatedHeight = Math.max(self.MIN_HEIGHT, y + desiredVisible * rowH + 10);
self.setSize([self.targetWidth, calculatedHeight]);
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
}
self.syncData();
});
return true;
}
} catch (e) {
console.error("[Rayko] Error loading preset:", e);
}
return false;
};
this.deletePreset = async function(name) {
try {
const response = await api.fetchApi("/rayko_delete_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
});
return response.ok;
} catch (e) {
console.error("[Rayko] Error deleting preset:", e);
return false;
}
};
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.drawRoundedRect = function(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
};
this.drawToolbarButton = function(ctx, text, icon, x, y, w, h, borderColor, isActive = true) {
const radius = 5;
ctx.fillStyle = "#242427";
this.drawRoundedRect(ctx, x, y, w, h, radius);
ctx.fill();
ctx.strokeStyle = isActive ? borderColor : "#444";
ctx.lineWidth = 1;
this.drawRoundedRect(ctx, x, y, w, h, radius);
ctx.stroke();
ctx.font = "bold 10px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const fullText = icon + " " + text;
ctx.fillStyle = isActive ? "#fff" : "#666";
ctx.fillText(fullText, x + w/2, y + h/2);
};
this.getStylesListStartY = function() {
const startY = 80;
const rowH = self.rowHeight;
let y = startY;
y += rowH + 10;
y += rowH + 10;
y += rowH + 10;
y += rowH + 15;
y += rowH + 5;
y += self.toolbarHeight + 8;
return y;
};
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, "⭐ FAVORITES", pad, y, self.size[0] - pad*2, rowH, "#FFD700");
this.clickZones.push({ type: "favorites", 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 + 5;
const toolbarY = y;
const toolbarH = self.toolbarHeight;
const availableWidth = self.size[0] - pad * 2;
const baseClearW = 80;
const baseResetW = 80;
const baseSaveW = 75;
const baseLoadW = 75;
const gap = 6;
const baseAvailableWidth = self.MIN_WIDTH - pad * 2;
const scale = Math.min(1.5, Math.max(1, availableWidth / baseAvailableWidth));
const clearW = Math.floor(baseClearW * scale);
const resetW = Math.floor(baseResetW * scale);
const saveW = Math.floor(baseSaveW * scale);
const loadW = Math.floor(baseLoadW * scale);
const totalW = clearW + resetW + saveW + loadW + gap * 3;
const startX = pad + (availableWidth - totalW) / 2;
self.drawToolbarButton(ctx, "Clear All", "🔴", startX, toolbarY, clearW, toolbarH, "#f44336", self.data.styles.length > 0);
this.clickZones.push({ type: "clear_all", x: startX, y: toolbarY, w: clearW, h: toolbarH });
const resetX = startX + clearW + gap;
self.drawToolbarButton(ctx, "Reset Size", "🔄", resetX, toolbarY, resetW, toolbarH, "#2196F3", self.manual_size);
this.clickZones.push({ type: "reset_size", x: resetX, y: toolbarY, w: resetW, h: toolbarH });
const saveX = resetX + resetW + gap;
self.drawToolbarButton(ctx, "Save", "💾", saveX, toolbarY, saveW, toolbarH, "#4CAF50", true);
this.clickZones.push({ type: "save_preset", x: saveX, y: toolbarY, w: saveW, h: toolbarH });
const loadX = saveX + saveW + gap;
self.drawToolbarButton(ctx, "Load", "📂", loadX, toolbarY, loadW, toolbarH, "#2196F3", true);
this.clickZones.push({ type: "load_preset", x: loadX, y: toolbarY, w: loadW, h: toolbarH });
y += toolbarH + 8;
let maxVisibleStyles = Math.max(1, Math.floor((self.size[1] - y - 10) / rowH));
if (!self.manual_size) {
const desiredVisible = Math.min(self.data.styles.length, 10);
const calculatedHeight = Math.max(self.MIN_HEIGHT, y + desiredVisible * rowH + 10);
if (Math.abs(self.size[1] - calculatedHeight) > 1) {
self.isAutoResizing = true;
self.setSize([self.targetWidth, calculatedHeight]);
self.isAutoResizing = false;
}
const currentAvailableHeight = calculatedHeight - y - 10;
maxVisibleStyles = Math.max(1, Math.floor(currentAvailableHeight / rowH));
}
const maxOffset = Math.max(0, self.data.styles.length - maxVisibleStyles);
if (self.scrollOffset > maxOffset) {
self.scrollOffset = maxOffset;
}
const visibleStart = self.scrollOffset;
const visibleEnd = Math.min(visibleStart + maxVisibleStyles, self.data.styles.length);
const rightPanelWidth = 40;
const toggleStartX = 20;
const toggleRadius = 7;
const nameStartX = 42;
for (let i = 0; i < visibleEnd - visibleStart; i++) {
const dataIdx = visibleStart + i;
const row = self.data.styles[dataIdx];
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: dataIdx, 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: dataIdx, 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: dataIdx, x: delX, y: styleY, w: 30, h: h });
}
if (self.data.styles.length > maxVisibleStyles) {
if (self.scrollOffset > 0) {
const indicatorY = y - 2;
ctx.fillStyle = "rgba(255, 215, 0, 0.6)";
ctx.beginPath();
ctx.moveTo(self.size[0]/2 - 8, indicatorY);
ctx.lineTo(self.size[0]/2 + 8, indicatorY);
ctx.lineTo(self.size[0]/2, indicatorY - 8);
ctx.closePath();
ctx.fill();
}
if (visibleEnd < self.data.styles.length) {
const indicatorY = y + (visibleEnd - visibleStart) * rowH + 2;
ctx.fillStyle = "rgba(255, 215, 0, 0.6)";
ctx.beginPath();
ctx.moveTo(self.size[0]/2 - 8, indicatorY);
ctx.lineTo(self.size[0]/2 + 8, indicatorY);
ctx.lineTo(self.size[0]/2, indicatorY + 8);
ctx.closePath();
ctx.fill();
}
}
};
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 === "favorites") {
self.showFavoritesMenu(e);
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.scrollOffset = 0;
self.manual_size = false;
self.syncData();
requestAnimationFrame(() => {
const startY = 80;
const rowH = self.rowHeight;
let y = startY;
y += rowH + 10;
y += rowH + 10;
y += rowH + 10;
y += rowH + 15;
y += rowH + 5;
y += self.toolbarHeight + 8;
const desiredVisible = Math.min(self.data.styles.length, 10);
const calculatedHeight = Math.max(self.MIN_HEIGHT, y + desiredVisible * rowH + 10);
self.setSize([self.targetWidth, calculatedHeight]);
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
}
});
return true;
}
if (zone.type === "clear_all") {
if (self.data.styles.length === 0) return true;
self.data.styles = [];
self.scrollOffset = 0;
self.manual_size = false;
self.syncData();
requestAnimationFrame(() => {
self.setSize([self.targetWidth, self.MIN_HEIGHT]);
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
}
});
return true;
}
if (zone.type === "reset_size") {
self.manual_size = false;
self.scrollOffset = 0;
self.syncData();
self.updateUI();
return true;
}
if (zone.type === "save_preset") {
self.showSavePresetDialog(e);
return true;
}
if (zone.type === "load_preset") {
self.showLoadPresetMenu(e);
return true;
}
}
}
return false;
};
const onResize = this.onResize;
this.onResize = function(size) {
if (size[0] < self.MIN_WIDTH) size[0] = self.MIN_WIDTH;
if (size[1] < self.MIN_HEIGHT) size[1] = self.MIN_HEIGHT;
if (!self.isAutoResizing) {
self.manual_size = true;
self.syncData();
}
if (onResize) {
return onResize.apply(this, arguments);
}
};
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 && clickEvent.clientX !== undefined) {
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.toggleFavorite = async function(styleFullName, styleDisplayName) {
const file = self.data.active_csv_file;
if (!self.data.favorites[file]) {
self.data.favorites[file] = [];
}
const favs = self.data.favorites[file];
const idx = favs.findIndex(f => f.fullName === styleFullName);
if (idx > -1) {
favs.splice(idx, 1);
} else {
favs.push({ fullName: styleFullName, displayName: styleDisplayName });
}
await self.saveFavoritesToServer();
};
this.showFavoritesMenu = async function(clickEvent) {
await self.loadFavoritesFromServer();
const file = self.data.active_csv_file;
const favs = self.data.favorites[file] || [];
const existingMenu = document.getElementById("rayko-favorites-menu");
if (existingMenu) existingMenu.remove();
const menu = document.createElement("div");
menu.id = "rayko-favorites-menu";
menu.style.cssText = `position:fixed;background:#1a1a1a;border:2px solid #FFD700;border-radius:6px;max-height:400px;overflow:hidden;z-index:10001;box-shadow:0 4px 20px rgba(255,215,0,0.3);min-width:250px;display:flex;flex-direction:column;`;
const header = document.createElement("div");
header.textContent = `⭐ Favorites: ${file}`;
header.style.cssText = `padding:10px 12px;color:#fff;font-weight:bold;border-bottom:1px solid #333;background:#252525;flex-shrink:0;`;
menu.appendChild(header);
const contentContainer = document.createElement("div");
contentContainer.style.cssText = `overflow-y:auto;flex-grow:1;`;
menu.appendChild(contentContainer);
if (clickEvent && clickEvent.clientX !== undefined) {
menu.style.left = (clickEvent.clientX + 8) + "px";
menu.style.top = clickEvent.clientY + "px";
}
function isStyleAlreadyAdded(styleFullName) {
return self.data.styles.some(row =>
row.name === styleFullName && row.file === self.data.active_csv_file
);
}
function renderFavoritesContent() {
contentContainer.innerHTML = "";
const currentFavs = self.data.favorites[file] || [];
if (currentFavs.length === 0) {
const emptyMsg = document.createElement("div");
emptyMsg.textContent = "No favorites for this file yet.";
emptyMsg.style.cssText = `padding:15px;color:#888;font-size:12px;text-align:center;`;
contentContainer.appendChild(emptyMsg);
return;
}
for (const fav of currentFavs) {
const isAdded = isStyleAlreadyAdded(fav.fullName);
const item = document.createElement("div");
item.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:10px 12px;cursor:pointer;border-bottom:1px solid #333;font-size:13px;background:#1a1a1a;transition:background-color 0.15s;`;
item.onmouseover = () => item.style.backgroundColor = "#333";
item.onmouseout = () => item.style.backgroundColor = "#1a1a1a";
const textSpan = document.createElement("span");
textSpan.textContent = (isAdded ? "✓ " : "🎨 ") + fav.displayName;
textSpan.style.cssText = `flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${isAdded ? '#4CAF50' : '#ddd'};`;
const starSpan = document.createElement("span");
starSpan.textContent = "★";
starSpan.style.cssText = `color:#FFD700;font-size:16px;margin-left:10px;flex-shrink:0;cursor:pointer;`;
starSpan.onclick = async (ev) => {
ev.stopPropagation();
await self.toggleFavorite(fav.fullName, fav.displayName);
renderFavoritesContent();
};
item.onclick = (ev) => {
ev.stopPropagation();
self.addStyleRow(fav.fullName, fav.displayName);
renderFavoritesContent();
};
item.appendChild(textSpan);
item.appendChild(starSpan);
contentContainer.appendChild(item);
}
}
renderFavoritesContent();
const closeMenu = () => {
menu.remove();
document.removeEventListener("keydown", closeEsc, true);
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
};
const closeEsc = (ev) => {
if (ev.key === "Escape") closeMenu();
};
let closeTimer = null;
const closeDelay = 300;
menu.addEventListener("mouseleave", () => {
closeTimer = setTimeout(() => {
closeMenu();
}, closeDelay);
});
menu.addEventListener("mouseenter", () => {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
});
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";
}
}, 10);
document.body.appendChild(menu);
document.addEventListener("keydown", closeEsc, true);
};
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.expandedFolders = self.expandedFolders || {};
self.showStyleTreeMenu(tree, self.expandedFolders, 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:2px solid #9C27B0;border-radius:6px;max-height:400px;overflow:hidden;z-index:10001;box-shadow:0 4px 20px rgba(156,39,176,0.3);min-width:250px;display:flex;flex-direction:column;`;
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;flex-shrink:0;`;
menu.appendChild(header);
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = " Search styles...";
searchInput.style.cssText = `width:calc(100% - 24px);margin:8px 12px;padding:8px;background:#2a2a2a;border:1px solid #444;border-radius:4px;color:#fff;font-size:12px;outline:none;flex-shrink:0;`;
searchInput.autofocus = true;
menu.appendChild(searchInput);
const contentContainer = document.createElement("div");
contentContainer.id = "rayko-style-content";
contentContainer.style.cssText = `overflow-y:auto;flex-grow:1;`;
menu.appendChild(contentContainer);
let currentSearchQuery = "";
const closeMenu = () => {
self.menuPosition = null;
self.menuScrollPosition = 0;
menu.remove();
document.removeEventListener("keydown", closeEsc, true);
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
};
const closeEsc = (ev) => {
if (ev.key === "Escape") closeMenu();
};
let closeTimer = null;
const closeDelay = 300;
menu.addEventListener("mouseleave", () => {
closeTimer = setTimeout(() => {
closeMenu();
}, closeDelay);
});
menu.addEventListener("mouseenter", () => {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
});
searchInput.addEventListener("input", (e) => {
currentSearchQuery = e.target.value.toLowerCase();
renderContent();
});
function isStyleAlreadyAdded(styleFullName) {
return self.data.styles.some(row =>
row.name === styleFullName && row.file === self.data.active_csv_file
);
}
function renderStyleItem(container, styleFullName, styleDisplayName, displayText = null, indentLevel = 0) {
const isAdded = isStyleAlreadyAdded(styleFullName);
const item = document.createElement("div");
item.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:10px 12px;cursor:pointer;border-bottom:1px solid #333;font-size:13px;background:#1a1a1a;transition:background-color 0.15s;`;
if (indentLevel > 0) {
item.style.paddingLeft = (12 + indentLevel * 16) + "px";
}
const textSpan = document.createElement("span");
textSpan.textContent = (isAdded ? "✓ " : "🎨 ") + (displayText || styleDisplayName);
textSpan.style.cssText = `flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${isAdded ? '#4CAF50' : '#ddd'};`;
const starSpan = document.createElement("span");
const fileFavs = self.data.favorites[self.data.active_csv_file] || [];
const isFav = fileFavs.some(f => f.fullName === styleFullName);
starSpan.textContent = isFav ? "★" : "☆";
starSpan.style.cssText = `color: ${isFav ? '#FFD700' : '#888'}; font-size: 16px; margin-left: 10px; flex-shrink: 0; cursor: pointer;`;
starSpan.onclick = async (ev) => {
ev.stopPropagation();
await self.toggleFavorite(styleFullName, styleDisplayName);
renderContent();
};
item.onclick = (ev) => {
ev.stopPropagation();
self.addStyleRow(styleFullName, styleDisplayName);
renderContent();
};
item.onmouseover = () => item.style.backgroundColor = "#333";
item.onmouseout = () => item.style.backgroundColor = "#1a1a1a";
item.appendChild(textSpan);
item.appendChild(starSpan);
container.appendChild(item);
}
function renderContent() {
contentContainer.innerHTML = "";
const query = currentSearchQuery;
const matchesQuery = (displayName) => {
if (!query) return true;
return displayName.toLowerCase().includes(query);
};
if (query) {
const allMatchingStyles = [];
function collectStyles(node, folderPrefix = "") {
if (node._styles) {
for (const styleObj of node._styles) {
if (matchesQuery(styleObj.displayName)) {
allMatchingStyles.push({
fullName: styleObj.fullName,
displayName: styleObj.displayName,
folderPath: folderPrefix ? `${folderPrefix} / ${styleObj.displayName}` : styleObj.displayName
});
}
}
}
if (node._folders) {
for (const folderName of node._folders) {
collectStyles(node[folderName], folderPrefix ? `${folderPrefix} / ${folderName}` : folderName);
}
}
}
collectStyles(tree);
if (allMatchingStyles.length === 0) {
const noResult = document.createElement("div");
noResult.textContent = "No styles found";
noResult.style.cssText = `padding:10px 12px;color:#888;font-size:12px;text-align:center;`;
contentContainer.appendChild(noResult);
return;
}
for (const styleObj of allMatchingStyles) {
renderStyleItem(contentContainer, styleObj.fullName, styleObj.displayName, styleObj.folderPath);
}
} else {
if (tree._styles && tree._styles.length > 0) {
for (const styleObj of tree._styles) {
renderStyleItem(contentContainer, styleObj.fullName, styleObj.displayName);
}
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 = contentContainer.scrollTop;
expandedFolders[folderPath] = !isExpanded;
renderContent();
setTimeout(() => { contentContainer.scrollTop = savedScroll; }, 0);
};
container.appendChild(folderHeader);
if (isExpanded) {
if (folderData._styles && folderData._styles.length > 0) {
for (const styleObj of folderData._styles) {
renderStyleItem(container, styleObj.fullName, styleObj.displayName, null, level + 1);
}
}
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) {
contentContainer.scrollTop = currentScroll;
}
searchInput.focus();
}, 10);
document.body.appendChild(menu);
contentContainer.addEventListener("scroll", () => {
self.menuScrollPosition = contentContainer.scrollTop;
});
document.addEventListener("keydown", closeEsc, true);
};
this.showSavePresetDialog = function(e) {
const existingDialog = document.getElementById("rayko-save-preset-dialog");
if (existingDialog) existingDialog.remove();
const dialog = document.createElement("div");
dialog.id = "rayko-save-preset-dialog";
dialog.style.cssText = `position:fixed;background:#2a2a2a;padding:10px;border:1px solid #4CAF50;border-radius:6px;z-index:10000;box-shadow:0 4px 15px rgba(0,0,0,0.7);width:220px;`;
const label = document.createElement("div");
label.style.cssText = `color:#999;font-size:11px;margin-bottom:4px;`;
label.textContent = "Preset name:";
const input = document.createElement("input");
input.style.cssText = `width:100%;padding:5px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;margin-bottom:5px;font-size:12px;box-sizing:border-box;`;
const btns = document.createElement("div");
btns.style.cssText = `display:flex;gap:5px;`;
const okBtn = document.createElement("button");
okBtn.style.cssText = `flex:1;padding:4px;background:#1a3a1a;color:#aaffaa;border:1px solid #4CAF50;border-radius:3px;cursor:pointer;font-size:11px;`;
okBtn.textContent = "OK";
const cancelBtn = document.createElement("button");
cancelBtn.style.cssText = `flex:1;padding:4px;background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:11px;`;
cancelBtn.textContent = "Cancel";
btns.appendChild(okBtn);
btns.appendChild(cancelBtn);
dialog.appendChild(label);
dialog.appendChild(input);
dialog.appendChild(btns);
let finalX = 100, finalY = 100;
if (e && e.clientX !== undefined && e.clientY !== undefined) {
finalX = e.clientX + 10;
finalY = e.clientY + 10;
}
if (finalX + 220 > window.innerWidth) finalX = window.innerWidth - 230;
if (finalY + 120 > window.innerHeight) finalY = window.innerHeight - 130;
if (finalX < 10) finalX = 10;
if (finalY < 10) finalY = 10;
dialog.style.left = finalX + "px";
dialog.style.top = finalY + "px";
document.body.appendChild(dialog);
setTimeout(() => input.focus(), 50);
const performSave = async () => {
const name = input.value.trim();
if (!name) return;
dialog.remove();
document.removeEventListener("pointerdown", closeOutside, true);
const success = await self.savePreset(name);
if (success) {
console.log(`[Rayko] Preset "${name}" saved`);
} else {
alert("Failed to save preset");
}
};
okBtn.onclick = (ev) => {
ev.stopPropagation();
performSave();
};
cancelBtn.onclick = (ev) => {
ev.stopPropagation();
dialog.remove();
document.removeEventListener("pointerdown", closeOutside, true);
};
input.onkeydown = (ev) => {
if (ev.key === "Enter") performSave();
if (ev.key === "Escape") {
dialog.remove();
document.removeEventListener("pointerdown", closeOutside, true);
}
};
const clickTime = Date.now();
const closeOutside = (ev) => {
if (Date.now() - clickTime < 300) return;
if (!dialog.contains(ev.target)) {
dialog.remove();
document.removeEventListener("pointerdown", closeOutside, true);
}
};
setTimeout(() => {
document.addEventListener("pointerdown", closeOutside, true);
}, 50);
};
this.showLoadPresetMenu = async function(e) {
const existingMenu = document.getElementById("rayko-load-preset-menu");
if (existingMenu) existingMenu.remove();
const menu = document.createElement("div");
menu.id = "rayko-load-preset-menu";
menu.style.cssText = `position:fixed;background:#1a1a1a;border:2px solid #2196F3;border-radius:6px;max-height:300px;overflow-y:auto;z-index:10001;box-shadow:0 4px 20px rgba(33,150,243,0.3);min-width:250px;`;
menu.innerHTML = `<div style="padding:8px;color:#999;text-align:center;">Loading...</div>`;
let finalX = 100, finalY = 100;
if (e && e.clientX !== undefined && e.clientY !== undefined) {
finalX = e.clientX + 10;
finalY = e.clientY + 10;
}
if (finalX + 250 > window.innerWidth) finalX = window.innerWidth - 260;
if (finalY + 300 > window.innerHeight) finalY = window.innerHeight - 310;
if (finalX < 10) finalX = 10;
if (finalY < 10) finalY = 10;
menu.style.left = finalX + "px";
menu.style.top = finalY + "px";
document.body.appendChild(menu);
const clickTime = Date.now();
const closeOutside = (ev) => {
if (Date.now() - clickTime < 300) return;
if (!menu.contains(ev.target)) {
menu.remove();
document.removeEventListener("pointerdown", closeOutside, true);
}
};
setTimeout(() => {
document.addEventListener("pointerdown", closeOutside, true);
}, 50);
try {
const presets = await self.loadPresetsList();
menu.innerHTML = "";
if (presets.length === 0) {
menu.textContent = "No presets found";
return;
}
presets.forEach(name => {
const row = document.createElement("div");
row.style.cssText = `display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid #333;`;
const nameSpan = document.createElement("span");
nameSpan.textContent = name;
nameSpan.style.cssText = `flex:1;cursor:pointer;color:#ccc;font-size:12px;`;
nameSpan.onmouseenter = () => nameSpan.style.background = "#3a3a3a";
nameSpan.onmouseleave = () => nameSpan.style.background = "transparent";
nameSpan.onclick = async (ev) => {
ev.stopPropagation();
menu.remove();
document.removeEventListener("pointerdown", closeOutside, true);
const success = await self.loadPreset(name);
if (!success) {
alert("Failed to load preset");
}
};
const deleteBtn = document.createElement("span");
deleteBtn.textContent = "❌";
deleteBtn.style.cssText = `cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;`;
deleteBtn.onmouseenter = () => {
deleteBtn.style.opacity = "1";
deleteBtn.style.transform = "scale(1.2)";
};
deleteBtn.onmouseleave = () => {
deleteBtn.style.opacity = "0.7";
deleteBtn.style.transform = "scale(1)";
};
deleteBtn.onclick = async (ev) => {
ev.stopPropagation();
if (confirm(`Delete preset "${name}"?`)) {
const success = await self.deletePreset(name);
if (success) {
menu.remove();
document.removeEventListener("pointerdown", closeOutside, true);
self.showLoadPresetMenu(e);
} else {
alert("Failed to delete preset");
}
}
};
row.appendChild(nameSpan);
row.appendChild(deleteBtn);
menu.appendChild(row);
});
} catch (err) {
menu.textContent = "Error loading presets";
}
};
this.addStyleRow = function(styleFullName, styleDisplayName) {
const exists = self.data.styles.some(row => row.name === styleFullName && row.file === self.data.active_csv_file);
if (exists) return;
self.data.styles.push({ name: styleFullName, display_name: styleDisplayName, file: self.data.active_csv_file, enabled: true });
self.scrollOffset = 0;
self.manual_size = false;
self.syncData();
requestAnimationFrame(() => {
const startY = 80;
const rowH = self.rowHeight;
let y = startY;
y += rowH + 10;
y += rowH + 10;
y += rowH + 10;
y += rowH + 15;
y += rowH + 5;
y += self.toolbarHeight + 8;
const desiredVisible = Math.min(self.data.styles.length, 10);
const calculatedHeight = Math.max(self.MIN_HEIGHT, y + desiredVisible * rowH + 10);
self.setSize([self.targetWidth, calculatedHeight]);
if (self.graph) {
self.graph.setDirtyCanvas(true, true);
setTimeout(() => self.graph.setDirtyCanvas(true, true), 50);
setTimeout(() => self.graph.setDirtyCanvas(true, true), 100);
setTimeout(() => self.graph.setDirtyCanvas(true, true), 150);
}
});
};
const onSerialize = this.onSerialize;
this.onSerialize = function(o) {
this.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 };
this.expandedFolders = restoredData.expandedFolders || {};
this.manual_size = restoredData.manual_size || false;
this.scrollOffset = restoredData.scrollOffset || 0;
if (!this.data.csv_list.length && restoredData.csv_list) {
this.data.csv_list = restoredData.csv_list;
}
this.setSize([
Math.max(this.size[0], this.MIN_WIDTH),
Math.max(this.size[1], this.MIN_HEIGHT)
]);
this.graph?.setDirtyCanvas(true, true);
}
} catch (e) {
console.error("[Rayko] Error restoring data:", e);
}
}
};
this.loadCSVList();
this.migrateFavorites();
return result;
};
}
}
});