Adding the RS Load Images From Dir node
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Raykosan (RaykoStudio)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import glob
|
||||
import torch
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
from PIL import Image
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class RSLoadImagesFromDir:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Dict[str, Any]]:
|
||||
return {
|
||||
"required": {
|
||||
"folder_path": ("STRING", {"default": "", "multiline": False}),
|
||||
"filter_type": (["*.*", "*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp", "*.gif", "custom"], {"default": "*.png"}),
|
||||
"start_index": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}),
|
||||
"end_index": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"custom_filter": ("STRING", {"default": "*.png", "multiline": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
|
||||
RETURN_NAMES = ("image", "filename", "folder_path", "number_of_files")
|
||||
OUTPUT_IS_LIST = (True, True, False, False)
|
||||
FUNCTION = "load_images"
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
DESCRIPTION = "Load images from directory. Preserves original size and alpha channel."
|
||||
|
||||
def load_images(
|
||||
self,
|
||||
folder_path: str,
|
||||
filter_type: str,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
custom_filter: str = "*.png"
|
||||
):
|
||||
if not folder_path or not os.path.isdir(folder_path):
|
||||
raise ValueError(f"Invalid folder path: {folder_path}")
|
||||
|
||||
pattern = custom_filter if filter_type == "custom" else filter_type
|
||||
search_pattern = os.path.join(folder_path, pattern)
|
||||
|
||||
file_list = sorted(
|
||||
[f for f in glob.glob(search_pattern) if os.path.isfile(f)],
|
||||
key=lambda x: os.path.basename(x).lower()
|
||||
)
|
||||
|
||||
total_files = len(file_list)
|
||||
images_list: List[torch.Tensor] = []
|
||||
filenames_list: List[str] = []
|
||||
|
||||
if total_files == 0:
|
||||
return (images_list, filenames_list, folder_path, total_files)
|
||||
|
||||
start_idx = max(0, min(start_index - 1, total_files - 1))
|
||||
end_idx = max(start_idx, min(end_index - 1, total_files - 1))
|
||||
|
||||
for idx in range(start_idx, end_idx + 1):
|
||||
filepath = file_list[idx]
|
||||
filename = os.path.basename(filepath)
|
||||
|
||||
try:
|
||||
img = Image.open(filepath)
|
||||
img_np = np.array(img).astype(np.float32) / 255.0
|
||||
|
||||
if img_np.ndim == 2:
|
||||
img_np = np.stack([img_np] * 3, axis=-1)
|
||||
|
||||
img_tensor = torch.from_numpy(img_np).unsqueeze(0)
|
||||
images_list.append(img_tensor)
|
||||
filenames_list.append(filename)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[RaykoStudio] Error loading {filename}: {e}")
|
||||
|
||||
return (images_list, filenames_list, folder_path, total_files)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RSLoadImagesFromDir": RSLoadImagesFromDir}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RSLoadImagesFromDir": "🦊 RS Load Images From Dir"}
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.33.2"
|
||||
__version__ = "0.34.0"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.33.2",
|
||||
"version": "0.34.0",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"Rayko_Insert_Crop.py"
|
||||
"Rayko_Intermediate_Spline_Mask.py"
|
||||
"Rayko_Last_Frame.py"
|
||||
"Rayko_Load_Images_From_Dir.py"
|
||||
"Rayko_Loop_Switch.py"
|
||||
"Rayko_LoRA Loader.py"
|
||||
"Rayko_Models_Loader.py"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.33.2"
|
||||
version = "0.34.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
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", "*.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 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");
|
||||
|
||||
[wPath, wFilter, wStart, wEnd, wCustom].forEach(w => {
|
||||
if (w) {
|
||||
w.hidden = true;
|
||||
if (w.element) w.element.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
if (wPath) wPath.serializeValue = () => node.data.folder_path;
|
||||
if (wFilter) wFilter.serializeValue = () => node.data.filter_type;
|
||||
if (wStart) wStart.serializeValue = () => parseInt(node.data.start_index);
|
||||
if (wEnd) wEnd.serializeValue = () => parseInt(node.data.end_index);
|
||||
if (wCustom) wCustom.serializeValue = () => node.data.custom_filter;
|
||||
|
||||
if (wPath?.value) node.data.folder_path = wPath.value;
|
||||
if (wFilter?.value) node.data.filter_type = wFilter.value;
|
||||
if (wStart?.value) node.data.start_index = parseInt(wStart.value);
|
||||
if (wEnd?.value) node.data.end_index = parseInt(wEnd.value);
|
||||
if (wCustom?.value) node.data.custom_filter = wCustom.value;
|
||||
|
||||
node.uiElements = [];
|
||||
node.dropdownMenu = null;
|
||||
node.customModal = null;
|
||||
|
||||
const syncData = () => {
|
||||
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 (node.graph) node.graph.changeTracker?.dispatchEvent(new Event("change"));
|
||||
node.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
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 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 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(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;
|
||||
|
||||
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;
|
||||
const pathDisplayY = currentY;
|
||||
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.strokeStyle = "#333";
|
||||
drawRoundedRect(ctx, padding, pathDisplayY, w - padding * 2, pathDisplayHeight, borderRadius);
|
||||
|
||||
const pathText = node.data.folder_path || "Folder not selected";
|
||||
const maxTextWidth = w - padding * 2 - 10;
|
||||
const displayPath = truncatePath(ctx, pathText, maxTextWidth);
|
||||
|
||||
ctx.fillStyle = node.data.folder_path ? "#aaa" : "#666";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(displayPath, padding + 5, pathDisplayY + 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 });
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
"Введите путь к папке",
|
||||
this.data.folder_path,
|
||||
(result) => {
|
||||
if (result !== null) {
|
||||
this.data.folder_path = result.trim();
|
||||
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;
|
||||
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);
|
||||
syncData();
|
||||
return true;
|
||||
}
|
||||
if (el.type === "btn_start_plus") {
|
||||
this.data.start_index = parseInt(this.data.start_index) + 1;
|
||||
syncData();
|
||||
return true;
|
||||
}
|
||||
if (el.type === "val_start") {
|
||||
showCustomPrompt(
|
||||
"Начальный индекс (мин. 1)",
|
||||
String(this.data.start_index),
|
||||
(result) => {
|
||||
if (result !== null) {
|
||||
this.data.start_index = Math.max(1, parseInt(result) || 1);
|
||||
syncData();
|
||||
}
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (el.type === "btn_end_minus") {
|
||||
this.data.end_index = Math.max(1, parseInt(this.data.end_index) - 1);
|
||||
syncData();
|
||||
return true;
|
||||
}
|
||||
if (el.type === "btn_end_plus") {
|
||||
this.data.end_index = parseInt(this.data.end_index) + 1;
|
||||
syncData();
|
||||
return true;
|
||||
}
|
||||
if (el.type === "val_end") {
|
||||
showCustomPrompt(
|
||||
"Конечный индекс",
|
||||
String(this.data.end_index),
|
||||
(result) => {
|
||||
if (result !== null) {
|
||||
this.data.end_index = Math.max(1, parseInt(result) || 1);
|
||||
syncData();
|
||||
}
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (el.type === "val_custom") {
|
||||
showCustomPrompt(
|
||||
"Пользовательский фильтр (например, *_mask.png)",
|
||||
this.data.custom_filter,
|
||||
(result) => {
|
||||
if (result !== null) {
|
||||
this.data.custom_filter = result.trim() || "*.png";
|
||||
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);
|
||||
};
|
||||
|
||||
syncData();
|
||||
updateNodeSize();
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user