Add files via upload
This commit is contained in:
@@ -0,0 +1,690 @@
|
||||
# IFLoadImagesNode.py
|
||||
import os
|
||||
import re
|
||||
import torch
|
||||
import glob
|
||||
import hashlib
|
||||
import logging
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
import folder_paths
|
||||
import shutil
|
||||
from typing import Tuple, List, Dict, Optional
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def numerical_sort_key(path):
|
||||
"""Sort file paths by numerical order in filenames"""
|
||||
parts = re.split('([0-9]+)', os.path.basename(path))
|
||||
parts[1::2] = map(int, parts[1::2]) # Convert number parts to integers
|
||||
return parts
|
||||
|
||||
class ImageManager:
|
||||
THUMBNAIL_PREFIX = "thb_"
|
||||
PATH_SEPARATOR = "___"
|
||||
SUBFOLDER_PREFIX = "dir"
|
||||
LEVEL_SEPARATOR = "--"
|
||||
THUMBNAIL_PREFIX = "thb_"
|
||||
THUMBNAIL_SIZE = (300, 300)
|
||||
|
||||
VALID_EXTENSIONS = {
|
||||
"none": {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"},
|
||||
"png": {".png"},
|
||||
"jpg": {".jpg", ".jpeg"},
|
||||
"webp": {".webp"},
|
||||
"gif": {".gif"},
|
||||
"bmp": {".bmp"}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def sanitize_path_component(component: str) -> str:
|
||||
"""Sanitize path components for safe filename use"""
|
||||
# Replace problematic characters but maintain readability
|
||||
sanitized = re.sub(r'[\\/:*?"<>|]', '_', component)
|
||||
return sanitized.strip()
|
||||
|
||||
@staticmethod
|
||||
def encode_path_to_filename(original_path: str, base_path: str) -> str:
|
||||
"""
|
||||
Convert a full file path to an encoded thumbnail filename that preserves hierarchy
|
||||
Format: thb_dir--level1--level2--level3___filename.ext
|
||||
"""
|
||||
try:
|
||||
# Normalize paths
|
||||
original_path = os.path.abspath(original_path)
|
||||
base_path = os.path.abspath(base_path)
|
||||
|
||||
# Get relative path components
|
||||
rel_path = os.path.relpath(original_path, base_path)
|
||||
dir_path = os.path.dirname(rel_path)
|
||||
filename = os.path.basename(rel_path)
|
||||
|
||||
if dir_path and dir_path != '.':
|
||||
# Split directory path and sanitize each component
|
||||
dir_parts = [ImageManager.sanitize_path_component(p)
|
||||
for p in dir_path.split(os.sep)]
|
||||
# Create encoded directory string
|
||||
dir_encoded = (f"{ImageManager.SUBFOLDER_PREFIX}"
|
||||
f"{ImageManager.LEVEL_SEPARATOR}"
|
||||
f"{ImageManager.LEVEL_SEPARATOR.join(dir_parts)}")
|
||||
# Combine with filename
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{dir_encoded}{ImageManager.PATH_SEPARATOR}{filename}"
|
||||
else:
|
||||
# No subdirectories
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{filename}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error encoding path {original_path}: {e}")
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{os.path.basename(original_path)}"
|
||||
|
||||
@staticmethod
|
||||
def decode_thumbnail_name(thumbnail_name: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Decode a thumbnail filename back into path components and original filename
|
||||
Returns (path_components, filename)
|
||||
"""
|
||||
if not thumbnail_name.startswith(ImageManager.THUMBNAIL_PREFIX):
|
||||
return [], thumbnail_name
|
||||
|
||||
# Remove prefix
|
||||
name_without_prefix = thumbnail_name[len(ImageManager.THUMBNAIL_PREFIX):]
|
||||
|
||||
# Split into directory part and filename
|
||||
parts = name_without_prefix.split(ImageManager.PATH_SEPARATOR)
|
||||
|
||||
if len(parts) == 2:
|
||||
# Has directory information
|
||||
dir_part, filename = parts
|
||||
if dir_part.startswith(ImageManager.SUBFOLDER_PREFIX):
|
||||
# Extract directory levels
|
||||
dir_levels = dir_part[len(ImageManager.SUBFOLDER_PREFIX):].split(ImageManager.LEVEL_SEPARATOR)
|
||||
# Remove empty strings
|
||||
dir_levels = [level for level in dir_levels if level]
|
||||
return dir_levels, filename
|
||||
|
||||
# No directory information
|
||||
return [], parts[-1]
|
||||
|
||||
@staticmethod
|
||||
def get_original_path(thumbnail_name: str, base_path: str) -> str:
|
||||
"""Convert a thumbnail name back to its original file path"""
|
||||
try:
|
||||
dir_levels, filename = ImageManager.decode_thumbnail_name(thumbnail_name)
|
||||
|
||||
if dir_levels:
|
||||
# Reconstruct path with proper system separators
|
||||
subpath = os.path.join(*dir_levels) if dir_levels else ""
|
||||
return os.path.normpath(os.path.join(base_path, subpath, filename))
|
||||
else:
|
||||
return os.path.normpath(os.path.join(base_path, filename))
|
||||
except Exception as e:
|
||||
logger.error(f"Error decoding thumbnail name {thumbnail_name}: {e}")
|
||||
return os.path.join(base_path, thumbnail_name.replace(ImageManager.THUMBNAIL_PREFIX, ""))
|
||||
|
||||
@staticmethod
|
||||
def normalize_path(path: str) -> str:
|
||||
"""Normalize path separators to system format"""
|
||||
return os.path.normpath(path.replace('\\', os.sep).replace('/', os.sep))
|
||||
|
||||
@staticmethod
|
||||
def get_relative_path(file_path: str, base_path: str) -> str:
|
||||
"""Get the relative path preserving all folder levels"""
|
||||
try:
|
||||
return os.path.relpath(file_path, base_path)
|
||||
except ValueError:
|
||||
# Handle case where paths are on different drives
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
def get_image_files(folder_path: str, include_subfolders: bool, filter_type: str) -> List[str]:
|
||||
"""Get list of image files with complete path hierarchy"""
|
||||
valid_exts = ImageManager.VALID_EXTENSIONS.get(filter_type.lower(), ImageManager.VALID_EXTENSIONS["none"])
|
||||
found_files = []
|
||||
|
||||
# Normalize the base folder path
|
||||
folder_path = ImageManager.normalize_path(folder_path)
|
||||
|
||||
def is_valid_image(filename: str) -> bool:
|
||||
return any(filename.lower().endswith(ext) for ext in valid_exts)
|
||||
|
||||
try:
|
||||
if include_subfolders:
|
||||
for root, _, filenames in os.walk(folder_path):
|
||||
for filename in filenames:
|
||||
if is_valid_image(filename):
|
||||
full_path = os.path.join(root, filename)
|
||||
# Store absolute paths for consistent handling
|
||||
found_files.append(os.path.abspath(full_path))
|
||||
else:
|
||||
with os.scandir(folder_path) as entries:
|
||||
for entry in entries:
|
||||
if entry.is_file() and is_valid_image(entry.name):
|
||||
found_files.append(os.path.abspath(entry.path))
|
||||
|
||||
return found_files
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting image files from {folder_path}: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def encode_path_to_filename(original_path: str, base_path: str) -> str:
|
||||
"""Convert a full file path to an encoded thumbnail filename preserving complete hierarchy"""
|
||||
try:
|
||||
# Normalize both paths
|
||||
original_path = ImageManager.normalize_path(original_path)
|
||||
base_path = ImageManager.normalize_path(base_path)
|
||||
|
||||
# Get relative path from base_path
|
||||
rel_path = ImageManager.get_relative_path(original_path, base_path)
|
||||
|
||||
# Split path into components
|
||||
path_parts = rel_path.split(os.sep)
|
||||
|
||||
if len(path_parts) > 1:
|
||||
# Join all directory parts with PATH_SEPARATOR
|
||||
dirs = ImageManager.PATH_SEPARATOR.join(path_parts[:-1])
|
||||
filename = path_parts[-1]
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{dirs}{ImageManager.PATH_SEPARATOR}{filename}"
|
||||
else:
|
||||
# No subdirectories
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{rel_path}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error encoding path {original_path}: {e}")
|
||||
return f"{ImageManager.THUMBNAIL_PREFIX}{os.path.basename(original_path)}"
|
||||
|
||||
@staticmethod
|
||||
def decode_thumbnail_name(thumbnail_name: str) -> Tuple[List[str], str]:
|
||||
"""Decode a thumbnail filename back into path components and original filename"""
|
||||
if not thumbnail_name.startswith(ImageManager.THUMBNAIL_PREFIX):
|
||||
return [], thumbnail_name
|
||||
|
||||
# Remove prefix
|
||||
name_without_prefix = thumbnail_name[len(ImageManager.THUMBNAIL_PREFIX):]
|
||||
|
||||
# Split by path separator and handle escaped separators
|
||||
parts = name_without_prefix.split(ImageManager.PATH_SEPARATOR)
|
||||
|
||||
# Last part is the filename, everything else is path components
|
||||
return parts[:-1], parts[-1]
|
||||
|
||||
@staticmethod
|
||||
def get_original_path(thumbnail_name: str, base_path: str) -> str:
|
||||
"""Convert a thumbnail name back to its original file path"""
|
||||
try:
|
||||
path_parts, filename = ImageManager.decode_thumbnail_name(thumbnail_name)
|
||||
|
||||
# Normalize base path
|
||||
base_path = ImageManager.normalize_path(base_path)
|
||||
|
||||
if path_parts:
|
||||
# Reconstruct path with proper system separators
|
||||
subpath = os.path.join(*path_parts)
|
||||
return os.path.normpath(os.path.join(base_path, subpath, filename))
|
||||
else:
|
||||
return os.path.normpath(os.path.join(base_path, filename))
|
||||
except Exception as e:
|
||||
logger.error(f"Error decoding thumbnail name {thumbnail_name}: {e}")
|
||||
return os.path.join(base_path, thumbnail_name.replace(ImageManager.THUMBNAIL_PREFIX, ""))
|
||||
|
||||
@staticmethod
|
||||
def create_thumbnails(folder_path: str, include_subfolders: bool = True,
|
||||
filter_type: str = "none", sort_method: str = "alphabetical",
|
||||
start_index: int = 0, max_images: Optional[int] = None) -> Tuple[bool, str, List[str], Dict[str, int]]:
|
||||
try:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
thumbnail_paths = []
|
||||
image_order = {} # Track image order
|
||||
|
||||
# Normalize paths
|
||||
if not os.path.isabs(folder_path):
|
||||
folder_path = os.path.abspath(os.path.join(folder_paths.get_input_directory(), folder_path))
|
||||
folder_path = ImageManager.normalize_path(folder_path)
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
return False, f"Path not found: {folder_path}", [], {}
|
||||
|
||||
# Get and filter files
|
||||
files = ImageManager.get_image_files(folder_path, include_subfolders, filter_type)
|
||||
if not files:
|
||||
return False, "No valid images found in the specified path", [], {}
|
||||
|
||||
# Sort files
|
||||
files = sorted(files, key=numerical_sort_key if sort_method == "numerical"
|
||||
else os.path.getctime if sort_method == "date_created"
|
||||
else os.path.getmtime if sort_method == "date_modified"
|
||||
else str)
|
||||
|
||||
# Clean up existing thumbnails
|
||||
for f in os.listdir(input_dir):
|
||||
if f.startswith(ImageManager.THUMBNAIL_PREFIX):
|
||||
try:
|
||||
os.remove(os.path.join(input_dir, f))
|
||||
except Exception as e:
|
||||
logger.warning(f"Error removing old thumbnail {f}: {e}")
|
||||
|
||||
# Apply index and count limits
|
||||
start_idx = min(max(0, start_index), len(files))
|
||||
end_idx = len(files) if max_images is None else min(start_idx + max_images, len(files))
|
||||
selected_files = files[start_idx:end_idx]
|
||||
|
||||
# Create thumbnails with encoded paths - only for selected range
|
||||
for idx, file_path in enumerate(selected_files, start=start_idx):
|
||||
try:
|
||||
thumb_name = ImageManager.encode_path_to_filename(file_path, folder_path)
|
||||
thumb_path = os.path.join(input_dir, thumb_name)
|
||||
|
||||
with Image.open(file_path) as img:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
if img.mode in ('RGBA', 'LA'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'RGBA':
|
||||
background.paste(img, mask=img.split()[3])
|
||||
else:
|
||||
background.paste(img, mask=img.split()[1])
|
||||
img = background
|
||||
elif img.mode not in ('RGB', 'L'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
img.thumbnail(ImageManager.THUMBNAIL_SIZE, Image.Resampling.LANCZOS)
|
||||
img.save(thumb_path, "JPEG", quality=70, optimize=True)
|
||||
|
||||
thumbnail_paths.append(thumb_name)
|
||||
image_order[thumb_name] = idx # Store image index
|
||||
logger.info(f"Created thumbnail: {thumb_name} for {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating thumbnail for {file_path}: {e}")
|
||||
continue
|
||||
|
||||
if not thumbnail_paths:
|
||||
return False, "Failed to create any thumbnails", [], {}
|
||||
|
||||
return True, f"Created {len(thumbnail_paths)} thumbnails", thumbnail_paths, image_order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Thumbnail creation failed: {str(e)}")
|
||||
return False, f"Thumbnail creation failed: {str(e)}", [], {}
|
||||
|
||||
@staticmethod
|
||||
def backup_input_folder() -> Tuple[bool, str]:
|
||||
try:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
backup_dir = os.path.join(os.path.dirname(input_dir), "input_backup")
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
if not os.path.exists(backup_dir):
|
||||
os.makedirs(backup_dir)
|
||||
|
||||
# First, remove all thumbnail files
|
||||
for file in os.listdir(input_dir):
|
||||
if file.startswith(ImageManager.THUMBNAIL_PREFIX):
|
||||
os.remove(os.path.join(input_dir, file))
|
||||
|
||||
# Copy remaining files from input to backup
|
||||
for file in os.listdir(input_dir):
|
||||
file_path = os.path.join(input_dir, file)
|
||||
if os.path.isfile(file_path):
|
||||
shutil.copy2(file_path, backup_dir)
|
||||
|
||||
# Clear input directory
|
||||
for file in os.listdir(input_dir):
|
||||
file_path = os.path.join(input_dir, file)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
return True, "Input folder backed up successfully"
|
||||
except Exception as e:
|
||||
logger.error(f"Backup failed: {str(e)}")
|
||||
return False, f"Backup failed: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def restore_input_folder() -> Tuple[bool, str]:
|
||||
try:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
backup_dir = os.path.join(os.path.dirname(input_dir), "input_backup")
|
||||
|
||||
if not os.path.exists(backup_dir):
|
||||
return False, "Backup directory not found"
|
||||
|
||||
# Clear thumbnails first
|
||||
for file in os.listdir(input_dir):
|
||||
if file.startswith(ImageManager.THUMBNAIL_PREFIX):
|
||||
os.remove(os.path.join(input_dir, file))
|
||||
|
||||
# Restore original files
|
||||
for file in os.listdir(backup_dir):
|
||||
shutil.copy2(os.path.join(backup_dir, file), input_dir)
|
||||
|
||||
return True, "Input folder restored successfully"
|
||||
except Exception as e:
|
||||
logger.error(f"Restore failed: {str(e)}")
|
||||
return False, f"Restore failed: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def sort_files(files: List[str], sort_method: str) -> List[str]:
|
||||
"""Sort files based on selected method"""
|
||||
if sort_method == "numerical":
|
||||
return sorted(files, key=numerical_sort_key)
|
||||
elif sort_method == "date_created":
|
||||
return sorted(files, key=os.path.getctime)
|
||||
elif sort_method == "date_modified":
|
||||
return sorted(files, key=os.path.getmtime)
|
||||
else: # alphabetical
|
||||
return sorted(files)
|
||||
|
||||
class IFLoadImagesS:
|
||||
def __init__(self):
|
||||
self.path_cache = {} # Cache for path mapping
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
# Count available thumbnails
|
||||
available_images = len([f for f in os.listdir(input_dir)
|
||||
if f.startswith(ImageManager.THUMBNAIL_PREFIX)])
|
||||
available_images = max(1, available_images) # Ensure at least 1
|
||||
|
||||
# Get thumbnail files
|
||||
files = [f for f in os.listdir(input_dir)
|
||||
if f.startswith(ImageManager.THUMBNAIL_PREFIX)]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": (sorted(files), {"image_upload": True}),
|
||||
"input_path": ("STRING", {"default": ""}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "max": 9999}),
|
||||
"max_images": ("INT", {"default": available_images, "min": 1, "max": available_images}),
|
||||
"image_selected": ("BOOLEAN", {"default": False}),
|
||||
"available_image_count": ("INT", {
|
||||
"default": available_images,
|
||||
"min": 0,
|
||||
"max": 99999,
|
||||
"readonly": True
|
||||
}),
|
||||
"include_subfolders": ("BOOLEAN", {"default": True}),
|
||||
"sort_method": (["alphabetical", "numerical", "date_created", "date_modified"],),
|
||||
"filter_type": (["none", "png", "jpg", "jpeg", "webp", "gif", "bmp"],),
|
||||
},
|
||||
"hidden": {
|
||||
"image_name": "IMAGE_NAME",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "STRING", "INT")
|
||||
RETURN_NAMES = ("images", "masks", "image_paths", "filenames", "count_str", "count_int")
|
||||
OUTPUT_IS_LIST = (True, True, True, True, True, True)
|
||||
FUNCTION = "load_images"
|
||||
CATEGORY = "ImpactFrames💥🎞️"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, image, input_path="", start_index=0, max_images=1,
|
||||
include_subfolders=True, sort_method="numerical",
|
||||
filter_type="none", image_name="", unique_id=None):
|
||||
"""
|
||||
Properly handle all input parameters and return NaN to force updates
|
||||
This matches the input parameters from INPUT_TYPES
|
||||
"""
|
||||
try:
|
||||
# If we have a specific image selected, use its path
|
||||
if image and not image.startswith("thb_"):
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
if image_path:
|
||||
m = hashlib.sha256()
|
||||
with open(image_path, 'rb') as f:
|
||||
m.update(f.read())
|
||||
return m.digest().hex()
|
||||
|
||||
# For directory-based loads, return NaN to force updates
|
||||
return float("NaN")
|
||||
except Exception as e:
|
||||
logging.warning(f"Error in IS_CHANGED: {e}")
|
||||
return float("NaN")
|
||||
|
||||
def load_images(self, image="", input_path="", start_index=0, max_images=1,
|
||||
image_selected=False, available_image_count=0, include_subfolders=True,
|
||||
sort_method="numerical", filter_type="none", image_name="", unique_id=None):
|
||||
try:
|
||||
# Process input path
|
||||
abs_path = os.path.abspath(input_path if os.path.isabs(input_path)
|
||||
else os.path.join(folder_paths.get_input_directory(), input_path))
|
||||
|
||||
# Get all valid images first
|
||||
all_files = ImageManager.get_image_files(abs_path, include_subfolders, filter_type)
|
||||
if not all_files:
|
||||
logger.warning(f"No valid images found in {abs_path}")
|
||||
img_tensor, mask = self.load_placeholder()
|
||||
return ([img_tensor], [mask], [""], [""], ["0/0"], [0])
|
||||
|
||||
# Sort files
|
||||
all_files = ImageManager.sort_files(all_files, sort_method)
|
||||
|
||||
# Generate thumbnails
|
||||
success, _, all_thumbnails, image_order = ImageManager.create_thumbnails(
|
||||
abs_path, include_subfolders, filter_type, sort_method,
|
||||
start_index, max_images
|
||||
)
|
||||
|
||||
# Handle image selection
|
||||
if image_selected and image in image_order:
|
||||
start_index = image_order[image]
|
||||
max_images = 1
|
||||
|
||||
# Create path mapping
|
||||
self.path_cache = {
|
||||
thumb: orig for thumb, orig in zip(all_thumbnails, all_files)
|
||||
}
|
||||
|
||||
# Process selected range
|
||||
total_files = len(all_files)
|
||||
start_idx = min(max(0, start_index), total_files)
|
||||
end_idx = min(start_idx + max_images, total_files)
|
||||
|
||||
selected_files = all_files[start_idx:end_idx]
|
||||
selected_thumbnails = all_thumbnails[start_idx:end_idx]
|
||||
|
||||
# Process selected range
|
||||
total_files = len(all_files)
|
||||
start_idx = min(max(0, start_index), total_files)
|
||||
end_idx = min(start_idx + max_images, total_files)
|
||||
|
||||
selected_files = all_files[start_idx:end_idx]
|
||||
selected_thumbnails = all_thumbnails[start_idx:end_idx]
|
||||
|
||||
# Process selected files
|
||||
images = []
|
||||
masks = []
|
||||
paths = []
|
||||
filenames = []
|
||||
count_strs = []
|
||||
count_ints = []
|
||||
|
||||
for idx, (file_path, thumb_name) in enumerate(zip(selected_files, selected_thumbnails)):
|
||||
try:
|
||||
with Image.open(file_path) as img:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
if img.mode == 'I':
|
||||
img = img.point(lambda i: i * (1 / 255))
|
||||
image = img.convert('RGB')
|
||||
|
||||
image_array = np.array(image).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_array)[None,]
|
||||
|
||||
if 'A' in img.getbands():
|
||||
mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1. - torch.from_numpy(mask)
|
||||
else:
|
||||
mask = torch.zeros((image_array.shape[0], image_array.shape[1]),
|
||||
dtype=torch.float32, device="cpu")
|
||||
|
||||
images.append(image_tensor)
|
||||
masks.append(mask.unsqueeze(0))
|
||||
paths.append(file_path)
|
||||
filenames.append(os.path.basename(file_path))
|
||||
count_str = f"{idx + 1}/{total_files}"
|
||||
count_strs.append(count_str)
|
||||
count_ints.append(idx + 1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing image {file_path}: {e}")
|
||||
continue
|
||||
|
||||
if not images:
|
||||
img_tensor, mask = self.load_placeholder()
|
||||
return ([img_tensor], [mask], [""], [""], ["0/0"], [0])
|
||||
|
||||
ui_data = {
|
||||
"images": all_thumbnails,
|
||||
"current_thumbnails": selected_thumbnails,
|
||||
"total_images": len(all_thumbnails),
|
||||
"path_mapping": self.path_cache,
|
||||
"available_image_count": len(all_thumbnails),
|
||||
"image_order": image_order
|
||||
}
|
||||
|
||||
return {
|
||||
"ui": {"values": ui_data},
|
||||
"result": (images, masks, paths, filenames, count_strs, count_ints)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in load_images: {e}", exc_info=True)
|
||||
img_tensor, mask = self.load_placeholder()
|
||||
return ([img_tensor], [mask], [""], [""], ["error"], [0])
|
||||
|
||||
def load_placeholder(self):
|
||||
"""Creates and returns a placeholder image tensor and mask"""
|
||||
img = Image.new('RGB', (512, 512), color=(73, 109, 137))
|
||||
image_array = np.array(img).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_array)[None,]
|
||||
mask = torch.zeros((1, image_array.shape[0], image_array.shape[1]),
|
||||
dtype=torch.float32, device="cpu")
|
||||
return image_tensor, mask
|
||||
|
||||
def process_single_image(self, image_path: str):
|
||||
"""Process a single image and return appropriate outputs"""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
if img.mode == 'I':
|
||||
img = img.point(lambda i: i * (1 / 255))
|
||||
|
||||
image = img.convert("RGB")
|
||||
image_array = np.array(image).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_array)[None,]
|
||||
|
||||
if 'A' in img.getbands():
|
||||
mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1. - torch.from_numpy(mask)
|
||||
else:
|
||||
mask = torch.zeros((image_array.shape[0], image_array.shape[1]),
|
||||
dtype=torch.float32, device="cpu")
|
||||
|
||||
filename = os.path.basename(image_path)
|
||||
return ([image_tensor], [mask.unsqueeze(0)], [image_path], [filename], ["1/1"], [1])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing single image {image_path}: {e}")
|
||||
img_tensor, mask = self.load_placeholder()
|
||||
return ([img_tensor], [mask], [""], [""], ["error"], [0])
|
||||
|
||||
@PromptServer.instance.routes.post("/ifai/backup_input")
|
||||
async def backup_input_folder(request):
|
||||
try:
|
||||
success, message = ImageManager.backup_input_folder()
|
||||
return web.json_response({
|
||||
"success": success,
|
||||
"message": message
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in backup_input_folder route: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/ifai/restore_input")
|
||||
async def restore_input_folder(request):
|
||||
try:
|
||||
success, message = ImageManager.restore_input_folder()
|
||||
return web.json_response({
|
||||
"success": success,
|
||||
"message": message
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in restore_input_folder route: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/ifai/refresh_previews")
|
||||
async def refresh_previews(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
if not data.get("input_path"):
|
||||
raise ValueError("No input path provided")
|
||||
|
||||
# Extract all parameters including max_images
|
||||
max_images = data.get("max_images", None)
|
||||
if max_images is not None:
|
||||
max_images = int(max_images)
|
||||
|
||||
# Pass max_images to create_thumbnails
|
||||
success, message, thumbnails, image_order = ImageManager.create_thumbnails(
|
||||
data["input_path"],
|
||||
include_subfolders=data.get("include_subfolders", True),
|
||||
filter_type=data.get("filter_type", "none"),
|
||||
sort_method=data.get("sort_method", "alphabetical"),
|
||||
start_index=int(data.get("start_index", 0)),
|
||||
max_images=max_images # Pass the limit
|
||||
)
|
||||
|
||||
return web.json_response({
|
||||
"success": success,
|
||||
"message": message,
|
||||
"thumbnails": thumbnails,
|
||||
"total_images": len(thumbnails),
|
||||
"available_image_count": len(thumbnails),
|
||||
"image_order": image_order # Include image_order in response
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in refresh_previews route: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
|
||||
# Add route for widget refresh
|
||||
@PromptServer.instance.routes.post("/ifai/refresh_widgets")
|
||||
async def refresh_widgets(request):
|
||||
try:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||
files = sorted(files)
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"files": files
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in refresh_widgets route: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
|
||||
# Register node class
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"IF_LoadImagesS": IFLoadImagess
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"IF_LoadImagesS": "IF Load ImagesS 🖼️"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2024 ImpactFrames
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
import sys
|
||||
import folder_paths
|
||||
|
||||
from .IFLoadImagesSNode import IFLoadImagess
|
||||
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"IF_LoadImagesS": IFLoadImagess,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"IF_LoadImagesS": "IF Load Images 🖼️",
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
"WEB_DIRECTORY",
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
// IFLoadImagesNodeS.js
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
// Create base styles for buttons
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.if-button {
|
||||
background: var(--comfy-input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--input-text);
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.if-button:hover {
|
||||
background: var(--comfy-input-bg-hover);
|
||||
}
|
||||
|
||||
.if-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.if-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid #666;
|
||||
border-top: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: ifSpin 1s linear infinite;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes ifSpin {
|
||||
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
|
||||
function updateNodePreview(node, imageName) {
|
||||
if (!imageName || !node) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
node.imgs = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.warn(`Failed to load preview for ${imageName}`);
|
||||
};
|
||||
|
||||
// Get the input directory path
|
||||
const inputPathWidget = node.widgets?.find(w => w.name === "input_path");
|
||||
const inputPath = inputPathWidget?.value || "";
|
||||
|
||||
// Construct URL with proper path handling
|
||||
const params = `&type=input${app.getPreviewFormatParam?.() || ""}${app.getRandParam?.() || ""}`;
|
||||
|
||||
// Use the thumbnail name directly since it's already in the input directory
|
||||
img.src = api.apiURL(`/view?filename=${encodeURIComponent(imageName)}&type=input${params}`);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.IFLoadImagesNodeS",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== "IF_LoadImagesS") return;
|
||||
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = origOnNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Image widget setup
|
||||
const imageWidget = this.widgets?.find(w => w.name === "image");
|
||||
if (imageWidget) {
|
||||
// Store original methods
|
||||
const origSetValue = imageWidget.setValue;
|
||||
const origCallback = imageWidget.callback;
|
||||
|
||||
imageWidget.setValue = function(v, skip_callback) {
|
||||
const result = origSetValue?.call(this, v, skip_callback);
|
||||
if (v && !skip_callback) {
|
||||
updateNodePreview(this.node, v);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Update preview on value change
|
||||
imageWidget.callback = function(value) {
|
||||
if (origCallback) {
|
||||
origCallback.call(this, value);
|
||||
}
|
||||
updateNodePreview(this.node, this.value);
|
||||
};
|
||||
}
|
||||
|
||||
// Add management buttons
|
||||
const selectFolderBtn = this.addWidget("button", "select_folder", "Select Folder 📂", () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.webkitdirectory = true;
|
||||
input.directory = true;
|
||||
input.style.display = "none";
|
||||
document.body.appendChild(input);
|
||||
|
||||
input.onchange = async (e) => {
|
||||
try {
|
||||
if (!e.target.files.length) return;
|
||||
const files = e.target.files;
|
||||
|
||||
// Get folder path
|
||||
let folderPath = '';
|
||||
if (files[0].path) {
|
||||
folderPath = files[0].path.split(/(\/|\\)/);
|
||||
folderPath = folderPath.slice(0, -2).join('');
|
||||
} else if (files[0].webkitRelativePath) {
|
||||
folderPath = files[0].webkitRelativePath.split('/')[0];
|
||||
}
|
||||
|
||||
if (!folderPath) {
|
||||
throw new Error("Could not determine folder path");
|
||||
}
|
||||
|
||||
// Prompt user to confirm/edit path
|
||||
const userFullPath = prompt("Confirm or edit the full path:", folderPath);
|
||||
if (!userFullPath) {
|
||||
throw new Error("No path entered");
|
||||
}
|
||||
|
||||
// Update input_path widget
|
||||
const pathWidget = this.widgets.find(w => w.name === "input_path");
|
||||
if (pathWidget) {
|
||||
pathWidget.value = userFullPath;
|
||||
if (pathWidget.callback) pathWidget.callback(userFullPath);
|
||||
|
||||
if (pathWidget.inputEl) {
|
||||
pathWidget.inputEl.value = userFullPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh previews
|
||||
const refreshBtn = this.widgets.find(w => w.name === "refresh_preview");
|
||||
if (refreshBtn?.callback) {
|
||||
setTimeout(() => refreshBtn.callback(), 100);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Folder selection error:", error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
|
||||
|
||||
const backupBtn = this.addWidget("button", "backup_input", "Backup Input 💾",
|
||||
async () => {
|
||||
try {
|
||||
const response = await api.fetchApi("/ifai/backup_input", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
alert("Input folder backed up successfully");
|
||||
} catch (error) {
|
||||
console.error("Backup error:", error);
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const restoreBtn = this.addWidget("button", "restore_input", "Restore Input ♻️",
|
||||
async () => {
|
||||
try {
|
||||
const response = await api.fetchApi("/ifai/restore_input", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
alert("Input folder restored successfully");
|
||||
|
||||
// Refresh previews after restore
|
||||
const refreshBtn = this.widgets.find(w => w.name === "refresh_preview");
|
||||
if (refreshBtn?.callback) {
|
||||
setTimeout(() => refreshBtn.callback(), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Restore error:", error);
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Add refresh button
|
||||
const refreshBtn = this.addWidget("button", "refresh_preview", "Refresh Previews 🔄", async () => {
|
||||
// In the refresh button callback:
|
||||
try {
|
||||
const inputPath = this.widgets.find(w => w.name === "input_path")?.value;
|
||||
if (!inputPath) {
|
||||
alert("Please select a folder first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Include max_images in options
|
||||
const options = {
|
||||
input_path: inputPath,
|
||||
include_subfolders: this.widgets.find(w => w.name === "include_subfolders")?.value ?? true,
|
||||
sort_method: this.widgets.find(w => w.name === "sort_method")?.value ?? "alphabetical",
|
||||
filter_type: this.widgets.find(w => w.name === "filter_type")?.value ?? "none",
|
||||
max_images: this.widgets.find(w => w.name === "max_images")?.value
|
||||
};
|
||||
|
||||
const response = await api.fetchApi("/ifai/refresh_previews", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(options)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) throw new Error(result.error);
|
||||
|
||||
// Update widgets
|
||||
const imageWidget = this.widgets?.find(w => w.name === "image");
|
||||
const availableCountWidget = this.widgets?.find(w => w.name === "available_image_count");
|
||||
|
||||
if (imageWidget && result.thumbnails?.length) {
|
||||
imageWidget.options.values = result.thumbnails;
|
||||
imageWidget.value = result.thumbnails[0];
|
||||
this.imageOrder = result.image_order || {}; // Store the image order
|
||||
imageWidget.callback.call(imageWidget);
|
||||
}
|
||||
|
||||
if (availableCountWidget) {
|
||||
availableCountWidget.value = result.available_image_count;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error refreshing previews:", error);
|
||||
alert("Error refreshing previews: " + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Arrange widgets - move refresh button below image widget
|
||||
if (refreshBtn) {
|
||||
const widgets = this.widgets.splice(-1); // Only remove one widget (refreshBtn)
|
||||
this.widgets.splice(imageWidget ? this.widgets.indexOf(imageWidget) + 1 : 0, 0, ...widgets);
|
||||
}
|
||||
|
||||
|
||||
// Handle execution results
|
||||
this.onExecuted = function(output) {
|
||||
if (output?.ui?.values) {
|
||||
const imageWidget = this.widgets?.find(w => w.name === "image");
|
||||
const availableCountWidget = this.widgets?.find(w => w.name === "available_image_count");
|
||||
const maxImagesWidget = this.widgets?.find(w => w.name === "max_images");
|
||||
|
||||
if (imageWidget) {
|
||||
// Store path mapping and image order
|
||||
this.pathMapping = output.ui.values.path_mapping || {};
|
||||
this.imageOrder = output.ui.values.image_order || {};
|
||||
|
||||
// Update widget options
|
||||
if (output.ui.values.images) {
|
||||
imageWidget.options.values = output.ui.values.images;
|
||||
|
||||
// Update available count and limits
|
||||
const count = output.ui.values.available_image_count;
|
||||
if (availableCountWidget) {
|
||||
availableCountWidget.value = count;
|
||||
}
|
||||
if (maxImagesWidget) {
|
||||
maxImagesWidget.options.max = count;
|
||||
if (maxImagesWidget.value > count) {
|
||||
maxImagesWidget.value = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle current selection
|
||||
if (output.ui.values.current_thumbnails?.length > 0) {
|
||||
const currentValue = imageWidget.value;
|
||||
if (!this.pathMapping[currentValue]) {
|
||||
imageWidget.value = output.ui.values.current_thumbnails[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview
|
||||
if (imageWidget.value) {
|
||||
updateNodePreview(this, imageWidget.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
// API calls
|
||||
nodeType.prototype.backupInputFolder = async function() {
|
||||
try {
|
||||
this.showLoader();
|
||||
const response = await fetch("/ifai/backup_input", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
this.showMessage("Input folder backed up successfully");
|
||||
} catch (error) {
|
||||
console.error("Backup error:", error);
|
||||
this.showMessage(error.message, "error");
|
||||
} finally {
|
||||
this.hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.restoreInputFolder = async function() {
|
||||
try {
|
||||
this.showLoader();
|
||||
const response = await fetch("/ifai/restore_input", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
this.showMessage("Input folder restored successfully");
|
||||
} catch (error) {
|
||||
console.error("Restore error:", error);
|
||||
this.showMessage(error.message, "error");
|
||||
} finally {
|
||||
this.hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.refreshPreviews = async function() {
|
||||
try {
|
||||
const input_path = this.widgets.find(w => w.name === "input_path")?.value;
|
||||
if (!input_path) {
|
||||
throw new Error("Please select a folder first");
|
||||
}
|
||||
|
||||
const options = {};
|
||||
this.widgets.forEach(w => {
|
||||
if (["select_folder", "refresh_preview", "backup_input", "restore_input"].includes(w.name)) return;
|
||||
options[w.name] = w.value;
|
||||
});
|
||||
|
||||
this.showLoader();
|
||||
|
||||
const response = await fetch("/ifai/refresh_previews", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(options)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
this.showMessage(`Generated ${result.thumbnails.length} previews`);
|
||||
} catch (error) {
|
||||
console.error("Preview refresh error:", error);
|
||||
this.showMessage(error.message, "error");
|
||||
} finally {
|
||||
this.hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle widget changes
|
||||
const origOnWidgetChanged = nodeType.prototype.onWidgetChanged;
|
||||
nodeType.prototype.onWidgetChanged = function (name, value) {
|
||||
if (origOnWidgetChanged) {
|
||||
origOnWidgetChanged.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Auto-refresh on certain changes
|
||||
if (["include_subfolders", "filter_type", "sort_method"].includes(name)) {
|
||||
this.refreshPreviews();
|
||||
}
|
||||
};
|
||||
|
||||
// Add right-click menu options
|
||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
if (origGetExtraMenuOptions) {
|
||||
origGetExtraMenuOptions.apply(this, arguments);
|
||||
}
|
||||
|
||||
options.unshift(
|
||||
{
|
||||
content: "Select Folder",
|
||||
callback: () => {
|
||||
const btn = this.widgets.find(w => w.name === "select_folder");
|
||||
if (btn?.callback) btn.callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Refresh Previews",
|
||||
callback: () => {
|
||||
const btn = this.widgets.find(w => w.name === "refresh_preview");
|
||||
if (btn?.callback) btn.callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Backup Input Folder",
|
||||
callback: () => {
|
||||
const btn = this.widgets.find(w => w.name === "backup_input");
|
||||
if (btn?.callback) btn.callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Restore Input Folder",
|
||||
callback: () => {
|
||||
const btn = this.widgets.find(w => w.name === "restore_input");
|
||||
if (btn?.callback) btn.callback();
|
||||
}
|
||||
},
|
||||
null // separator
|
||||
);
|
||||
|
||||
return options;
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user