Initial release of ComfyUI Subfolder Image Loader

Features:
  - Subfolder navigation and dynamic image filtering
  - Support for all common image formats
  - Transparency mask extraction
  - Security validation and error handling
  - JavaScript frontend for real-time UI updates
This commit is contained in:
rdomunky
2025-07-19 10:08:50 -07:00
commit fe290b76ea
8 changed files with 828 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.claude/
# OS
.DS_Store
Thumbs.db
# Local test files
test_*
*.log

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 rdomunky
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.

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# ComfyUI Subfolder Image Loader
A ComfyUI custom node that enhances image loading with subfolder organization and dynamic filtering.
## Features
- **Subfolder Navigation**: Organize your input images into subfolders and select them easily
- **Dynamic Image Filtering**: Image list automatically updates when you change subfolder selection
- **Two-Stage Selection**: First select a subfolder, then choose an image from within it
- **Format Support**: PNG, JPG, JPEG, WebP, BMP, TIFF formats
- **Transparency Support**: Automatic alpha channel/mask extraction from RGBA images
- **Refresh Functionality**: Right-click menu option to refresh file listings
- **Security**: Path validation prevents directory traversal attacks
## Installation
### Via ComfyUI Manager (Recommended)
1. Open ComfyUI Manager
2. Search for "Subfolder Image Loader"
3. Install the node
### Manual Installation
1. Navigate to your ComfyUI custom nodes directory:
```bash
cd /path/to/ComfyUI/custom_nodes/
```
2. Clone this repository:
```bash
git clone https://github.com/rdomunky/comfyui-subfolderimageloader.git
```
3. Restart ComfyUI
## Usage
1. **Organize Images**: Place your images in subfolders within ComfyUI's `input/` directory:
```
ComfyUI/input/
├── portraits/
│ ├── person1.jpg
│ └── person2.png
├── landscapes/
│ ├── mountain.jpg
│ └── beach.png
└── textures/
├── wood.jpg
└── metal.png
```
2. **Add Node**: In ComfyUI, add the "Subfolder Image Loader" node (found in `image/loaders` category)
3. **Select Subfolder**: Choose a subfolder from the dropdown (or leave empty for root folder)
4. **Choose Image**: The image dropdown will automatically filter to show only images from the selected subfolder
5. **Load Image**: The node outputs the same data as the standard Load Image node:
- `IMAGE`: The loaded image tensor
- `MASK`: Alpha channel mask (if present)
- `STRING`: Filename
- `INT`: Width
- `INT`: Height
## Tips
- **Refresh Files**: Right-click the node → "🔄 Refresh Files" to update file listings without restarting
- **UI Quirk**: Due to ComfyUI limitations, the image dropdown display may require a mouse movement to visually update (functionality works correctly)
- **Empty Folders**: Subfolders with no images will still appear in the subfolder list
- **Nested Folders**: Only direct subfolders are supported (no nested subfolder navigation)
## Technical Details
- **Path Security**: All file paths are validated to prevent directory traversal
- **API Endpoint**: Provides `/subfolder_loader/refresh` for dynamic file listing updates
- **Error Handling**: Graceful fallback to empty image tensors on errors
- **Performance**: Includes optional file caching for improved performance
## Comparison with Standard Load Image
| Feature | Standard Load Image | Subfolder Image Loader |
|---------|-------------------|----------------------|
| Subfolder support | ❌ | ✅ |
| Dynamic filtering | ❌ | ✅ |
| File organization | ❌ | ✅ |
| Transparency masks | ✅ | ✅ |
| Same outputs | ✅ | ✅ |
## Troubleshooting
**Images not appearing**: Ensure images are in supported formats and located in ComfyUI's input directory or its subfolders.
**Subfolder not updating**: Try the refresh option or restart ComfyUI if subfolders don't appear.
**JavaScript not loading**: Make sure the `web/` directory and `WEB_DIRECTORY` in `__init__.py` are properly configured.
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
## License
This project is licensed under the MIT License - see the LICENSE file for details.

18
__init__.py Normal file
View File

@@ -0,0 +1,18 @@
# __init__.py
from .subfolder_loader import SubfolderImageLoader
# Web directory for frontend assets
WEB_DIRECTORY = "./web"
# Required: Maps node identifier to class
NODE_CLASS_MAPPINGS = {
"SubfolderImageLoader": SubfolderImageLoader,
}
# Optional: Maps identifier to display name
NODE_DISPLAY_NAME_MAPPINGS = {
"SubfolderImageLoader": "Subfolder Image Loader",
}
# Export for ComfyUI discovery
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']

91
fixsubfolder.patch Normal file
View File

@@ -0,0 +1,91 @@
--- a/web/js/subfolder_ui.js
+++ b/web/js/subfolder_ui.js
@@ -35,32 +35,46 @@ app.registerExtension({
const originalImageOptions = [...imageWidget.options.values];
// Function to filter images based on selected subfolder
const filterImages = (selectedSubfolder) => {
let filteredImages;
+ let displayNames;
if (!selectedSubfolder || selectedSubfolder === "") {
// Show only root images (no / in filename)
filteredImages = originalImageOptions.filter(img => !img.includes('/'));
+ displayNames = filteredImages;
} else {
// Show only images from selected subfolder
const prefix = selectedSubfolder + "/";
filteredImages = originalImageOptions
- .filter(img => img.startsWith(prefix))
- .map(img => img.substring(prefix.length)); // Remove prefix for display
+ .filter(img => img.startsWith(prefix));
+ displayNames = filteredImages
+ .map(img => img.substring(prefix.length)); // Remove prefix for display only
}
// Update image widget options
- imageWidget.options.values = filteredImages;
+ imageWidget.options.values = displayNames;
- // Update widget value if current selection is no longer valid
- if (filteredImages.length > 0 && !filteredImages.includes(imageWidget.value)) {
- imageWidget.value = filteredImages[0];
- imageWidget.callback?.(filteredImages[0]);
+ // Check if current value is still valid
+ let currentValueValid = false;
+ if (selectedSubfolder && imageWidget.value && !imageWidget.value.includes('/')) {
+ // If we have a plain filename, check if it exists in the display names
+ currentValueValid = displayNames.includes(imageWidget.value);
+ } else if (!selectedSubfolder && imageWidget.value && !imageWidget.value.includes('/')) {
+ // Root folder - check if current value exists in root
+ currentValueValid = displayNames.includes(imageWidget.value);
+ }
+
+ // Update widget value if current selection is no longer valid
+ if (!currentValueValid && displayNames.length > 0) {
+ imageWidget.value = displayNames[0];
+ imageWidget.callback?.(displayNames[0]);
}
// Force widget UI update
if (imageWidget.element) {
// Clear existing options
imageWidget.element.innerHTML = "";
// Add new options
- filteredImages.forEach(img => {
+ displayNames.forEach((img, index) => {
const option = document.createElement("option");
option.value = img;
option.textContent = img;
@@ -68,6 +82,9 @@ app.registerExtension({
});
imageWidget.element.value = imageWidget.value;
}
+
+ // Trigger graph change to update the node
+ node.graph?.change();
};
// Set up subfolder change handler
@@ -138,8 +155,20 @@ app.registerExtension({
content: "Open in new tab",
callback: () => {
const subfolder = node.widgets.find(w => w.name === "subfolder")?.value || "";
const filename = widget.value;
- const url = `/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder)}`;
+
+ // Construct the correct URL based on whether we're in a subfolder
+ let url;
+ if (subfolder) {
+ // For subfolder images, we need to pass just the filename
+ url = `/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder)}`;
+ } else {
+ // For root images, pass the filename as is
+ url = `/view?filename=${encodeURIComponent(filename)}`;
+ }
+
window.open(url, '_blank');
}
},

362
subfolder_loader.py Normal file
View File

@@ -0,0 +1,362 @@
# subfolder_loader.py
import os
import json
import logging
from typing import List, Tuple, Optional
import folder_paths
from PIL import Image
import numpy as np
import torch
# Optional: Add server route for refresh functionality
try:
import server
from aiohttp import web
@server.PromptServer.instance.routes.post("/subfolder_loader/refresh")
async def refresh_file_listings(request):
"""API endpoint to refresh file listings."""
try:
data = await request.json()
node_id = data.get('node_id')
subfolder = data.get('subfolder', '')
# Get fresh file listings
input_dir = folder_paths.get_input_directory()
subfolders = SubfolderImageLoader.get_subfolders(input_dir)
# Get filtered images for the specified subfolder
if subfolder:
filtered_images = SubfolderImageLoader.get_images_for_subfolder(subfolder)
else:
filtered_images = SubfolderImageLoader.get_images_for_subfolder("")
# Also get all images for client-side filtering if needed
all_images = SubfolderImageLoader.get_all_images_with_paths(input_dir)
return web.json_response({
'success': True,
'subfolders': subfolders,
'images': all_images, # All images with paths for client filtering
'filtered_images': filtered_images, # Pre-filtered images for the subfolder
'current_subfolder': subfolder
})
except Exception as e:
logging.error(f"Refresh error: {str(e)}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
except ImportError:
pass
class SubfolderImageLoader:
"""
Enhanced image loader with subfolder selection support.
This node allows you to organize your input images into subfolders and then
dynamically select images from specific subfolders. First select a subfolder,
then choose an image - the image list will automatically filter to show only
images from the selected subfolder.
Features:
- Subfolder navigation within ComfyUI's input directory
- Dynamic image filtering based on selected subfolder
- Support for PNG, JPG, JPEG, WebP, BMP, TIFF formats
- Alpha channel/transparency mask extraction
- Right-click menu option to refresh file listings
"""
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
# Get available subfolders
subfolders = cls.get_subfolders(input_dir)
# Start with images from root folder (no subfolder selected)
default_images = cls.get_images_for_subfolder("")
return {
"required": {
"subfolder": (subfolders, {
"default": subfolders[0] if subfolders else "",
"tooltip": "Select a subfolder from your input directory. Leave empty for root folder. The image list will update automatically when you change this."
}),
"image": (default_images if default_images else [""], {
"default": default_images[0] if default_images else "",
"tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection."
}),
},
"optional": {
"load_mask": ("BOOLEAN", {
"default": True,
"tooltip": "Extract alpha channel as mask from RGBA/transparent images. Disable if you don't need transparency masks."
}),
}
}
@classmethod
def get_images_for_subfolder(cls, subfolder: str = "") -> list:
"""Get filtered images for a specific subfolder."""
input_dir = folder_paths.get_input_directory()
all_images = cls.get_all_images_with_paths(input_dir)
if not subfolder or subfolder == "":
# Root folder - show only images without subfolder (no slash)
return [img for img in all_images if '/' not in img]
else:
# Specific subfolder - show only images from that subfolder, without prefix
prefix = subfolder + "/"
filtered = [img[len(prefix):] for img in all_images
if img.startswith(prefix) and '/' not in img[len(prefix):]]
return filtered
@classmethod
def get_subfolders(cls, base_path: str) -> List[str]:
"""Get list of subfolders in the base directory."""
if not os.path.exists(base_path):
return [""]
subfolders = [""] # Include root/no subfolder option
try:
for item in sorted(os.listdir(base_path)):
item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and not item.startswith('.'):
subfolders.append(item)
except PermissionError:
logging.warning(f"Permission denied accessing {base_path}")
return subfolders
@classmethod
def get_all_images_with_paths(cls, base_path: str) -> List[str]:
"""Get all images with their relative paths."""
all_images = []
if not os.path.exists(base_path):
return all_images
# Get images from root
root_images = cls.get_images_from_folder(base_path)
all_images.extend(root_images)
# Get images from each subfolder with path prefix
for subfolder in cls.get_subfolders(base_path):
if subfolder: # Skip empty root option
subfolder_path = os.path.join(base_path, subfolder)
images = cls.get_images_from_folder(subfolder_path)
# Add with subfolder prefix
for img in images:
all_images.append(f"{subfolder}/{img}")
return sorted(all_images)
@classmethod
def get_images_from_folder(cls, folder_path: str) -> List[str]:
"""Get image files from a specific folder."""
if not os.path.exists(folder_path):
return []
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif'}
images = []
try:
for file in sorted(os.listdir(folder_path)):
if os.path.splitext(file.lower())[1] in valid_extensions:
images.append(file)
except PermissionError:
logging.warning(f"Permission denied accessing {folder_path}")
return images
@classmethod
def VALIDATE_INPUTS(cls, subfolder="", image="", **kwargs):
"""Validate inputs before execution."""
if not image:
return "No image specified"
input_dir = folder_paths.get_input_directory()
# Handle the case where image might contain subfolder prefix
clean_image = image
actual_subfolder = subfolder
# If image contains a path separator, extract the subfolder and filename
if '/' in image:
parts = image.split('/')
if len(parts) == 2:
potential_subfolder, clean_image = parts
# Use the subfolder from the image path if no subfolder is explicitly set
if not actual_subfolder:
actual_subfolder = potential_subfolder
# Build the full path
if actual_subfolder:
file_path = os.path.join(input_dir, actual_subfolder, clean_image)
else:
file_path = os.path.join(input_dir, clean_image)
# Check if file exists
if not os.path.exists(file_path):
# Log for debugging
logging.error(f"File not found: {file_path}")
logging.error(f"Original - Subfolder: '{subfolder}', Image: '{image}'")
logging.error(f"Processed - Subfolder: '{actual_subfolder}', Clean image: '{clean_image}'")
return f"Image file not found: {clean_image}"
# Validate it's within the input directory
try:
file_path_abs = os.path.abspath(file_path)
input_dir_abs = os.path.abspath(input_dir)
if not file_path_abs.startswith(input_dir_abs):
return "Invalid file path: outside input directory"
except Exception:
return "Invalid file path"
return True
@classmethod
def IS_CHANGED(cls, subfolder="", image="", **kwargs):
"""Control when node re-executes."""
if not image:
return False
try:
input_dir = folder_paths.get_input_directory()
# Build path
if subfolder:
file_path = os.path.join(input_dir, subfolder, image)
else:
file_path = os.path.join(input_dir, image)
if os.path.exists(file_path):
return os.path.getmtime(file_path)
except Exception:
pass
return False
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT", "INT")
RETURN_NAMES = ("image", "mask", "filename", "width", "height")
CATEGORY = "image/loaders"
FUNCTION = "load_image"
DESCRIPTION = "Load images from subfolders with dynamic filtering. Organize your images in subfolders and select them easily."
def load_image(self, subfolder: str = "", image: str = "", load_mask: bool = True, **kwargs) -> Tuple:
"""
Load image from the specified subfolder.
Args:
subfolder: Selected subfolder name
image: Image filename or path (may contain subfolder prefix)
load_mask: Whether to extract alpha channel as mask
Returns:
Tuple of (image_tensor, mask_tensor, filename, width, height)
"""
try:
if not image:
raise ValueError("No image specified")
input_dir = folder_paths.get_input_directory()
# Handle the case where image might contain subfolder prefix
clean_image = image
actual_subfolder = subfolder
# If image contains a path separator, extract the subfolder and filename
if '/' in image:
parts = image.split('/')
if len(parts) == 2:
potential_subfolder, clean_image = parts
# Use the subfolder from the image path if no subfolder is explicitly set
if not actual_subfolder:
actual_subfolder = potential_subfolder
# Build the full path
if actual_subfolder:
file_path = os.path.join(input_dir, actual_subfolder, clean_image)
else:
file_path = os.path.join(input_dir, clean_image)
# Validate path is safe
file_path_abs = os.path.abspath(file_path)
input_dir_abs = os.path.abspath(input_dir)
if not file_path_abs.startswith(input_dir_abs):
raise ValueError("Invalid file path: outside input directory")
# Check file exists
if not os.path.exists(file_path):
raise FileNotFoundError(f"Image file not found: {file_path}")
# Load and process image
image_tensor, mask_tensor = self.process_image(file_path, load_mask)
# Get image dimensions
height, width = image_tensor.shape[1:3]
# Return just the clean filename, not the full path
return (image_tensor, mask_tensor, clean_image, width, height)
except Exception as e:
logging.error(f"Error loading image '{image}' from subfolder '{subfolder}': {str(e)}")
# Return empty tensors on error
empty_image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
empty_mask = torch.zeros((1, 512, 512), dtype=torch.float32)
return (empty_image, empty_mask, "error", 512, 512)
def process_image(self, file_path: str, load_mask: bool = True) -> Tuple[torch.Tensor, torch.Tensor]:
"""Process image file into tensors."""
with Image.open(file_path) as img:
# Store original mode
original_mode = img.mode
mask_array = None
# Handle different image modes
if img.mode == 'RGBA' and load_mask:
# Extract alpha channel as mask before conversion
mask_array = np.array(img.getchannel('A'))
img = img.convert('RGB')
elif img.mode == 'P':
# Convert palette images
if 'transparency' in img.info:
img = img.convert('RGBA')
if load_mask:
mask_array = np.array(img.getchannel('A'))
img = img.convert('RGB')
else:
img = img.convert('RGB')
elif img.mode == 'L':
# Convert grayscale to RGB
img = img.convert('RGB')
elif img.mode not in ['RGB']:
# Convert any other mode to RGB
img = img.convert('RGB')
# Convert to numpy array
image_array = np.array(img, dtype=np.float32) / 255.0
# Ensure we have 3 channels
if len(image_array.shape) == 2:
image_array = np.stack([image_array] * 3, axis=-1)
# Convert to tensor
image_tensor = torch.from_numpy(image_array)
# Add batch dimension
image_tensor = image_tensor.unsqueeze(0)
# Create mask tensor
if load_mask and mask_array is not None:
mask_tensor = torch.from_numpy(mask_array.astype(np.float32) / 255.0)
mask_tensor = mask_tensor.unsqueeze(0)
else:
# Create default mask (all opaque)
h, w = image_tensor.shape[1:3]
mask_tensor = torch.ones((1, h, w), dtype=torch.float32)
return image_tensor, mask_tensor

49
utils/file_helpers.py Normal file
View File

@@ -0,0 +1,49 @@
# utils/file_helpers.py
import os
import time
from typing import List, Dict, Optional
import logging
class FileCache:
"""Simple file listing cache to improve performance."""
def __init__(self, cache_timeout: int = 5):
self.cache_timeout = cache_timeout
self._cache: Dict[str, List[str]] = {}
self._timestamps: Dict[str, float] = {}
def get_files(self, directory: str, force_refresh: bool = False) -> List[str]:
"""Get cached file listing or refresh if needed."""
current_time = time.time()
if (not force_refresh and
directory in self._timestamps and
current_time - self._timestamps[directory] < self.cache_timeout):
return self._cache.get(directory, [])
# Refresh cache
files = self._scan_directory(directory)
self._cache[directory] = files
self._timestamps[directory] = current_time
return files
def _scan_directory(self, directory: str) -> List[str]:
"""Scan directory for image files."""
if not os.path.exists(directory):
return []
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif'}
files = []
try:
for file in os.listdir(directory):
if os.path.splitext(file.lower())[1] in valid_extensions:
files.append(file)
except PermissionError:
logging.warning(f"Permission denied accessing {directory}")
return sorted(files)
# Global cache instance
file_cache = FileCache()

View File

@@ -0,0 +1,150 @@
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
// Extension for SubfolderImageLoader dynamic input filtering
app.registerExtension({
name: "comfyui.SubfolderImageLoader",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SubfolderImageLoader") {
const originalNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = originalNodeCreated?.apply(this, arguments);
// Setup widget callbacks after node is fully created
setTimeout(() => {
const subfolderWidget = this.widgets?.find(w => w.name === "subfolder");
if (subfolderWidget) {
const originalCallback = subfolderWidget.callback;
subfolderWidget.callback = (value, ...args) => {
if (originalCallback) {
originalCallback.call(subfolderWidget, value, ...args);
}
this.updateImageList(value);
};
// Also run initial filtering based on current subfolder value
const currentSubfolder = subfolderWidget.value || "";
this.updateImageList(currentSubfolder);
} else {
console.error("SubfolderImageLoader: Could not find subfolder widget!");
}
}, 100);
return result;
};
// Method to update image list based on selected subfolder
nodeType.prototype.updateImageList = async function(selectedSubfolder) {
try {
// Get the image widget
const imageWidget = this.widgets?.find(w => w.name === "image");
if (!imageWidget) {
return;
}
// Fetch updated image list from the backend
const response = await fetch("/subfolder_loader/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
node_id: this.id,
subfolder: selectedSubfolder || ""
})
});
if (!response.ok) {
console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText);
return;
}
const data = await response.json();
if (!data.success) {
console.error("SubfolderImageLoader: Server error:", data.error);
return;
}
// Use the pre-filtered images from the server
const filteredImages = data.filtered_images || [];
// Remove and recreate the image widget
const imageIndex = this.widgets.findIndex(w => w.name === "image");
if (imageIndex >= 0) {
// Store the old widget's callback for reference
const oldWidget = this.widgets[imageIndex];
const oldCallback = oldWidget.callback;
// Remove the old widget
this.widgets.splice(imageIndex, 1);
// Determine the new value - always use first image from filtered list
const newValue = filteredImages.length > 0 ? filteredImages[0] : "";
// Create new widget with updated options
const newWidget = this.addWidget(
"combo",
"image",
newValue,
(value) => {
// Call original callback if it existed
if (oldCallback) {
oldCallback.call(newWidget, value);
}
},
{
values: filteredImages.length > 0 ? filteredImages : [""]
}
);
// Move the new widget to the correct position
if (imageIndex < this.widgets.length - 1) {
const widget = this.widgets.pop();
this.widgets.splice(imageIndex, 0, widget);
}
// Force update the node's properties
this.setProperty("image", newValue);
// Trigger the widget callback to ensure the change is registered
if (newWidget.callback) {
newWidget.callback(newValue);
}
}
// Force UI update
// Note: ComfyUI has a quirk where combo widget visual updates sometimes require
// mouse movement to refresh the display. The functionality works correctly,
// but the visual update may be delayed until the next mouse interaction.
this.setDirtyCanvas(true, true);
} catch (error) {
console.error("SubfolderImageLoader: Error updating image list:", error);
}
};
// Override the original getExtraMenuOptions to add refresh option
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
const result = originalGetExtraMenuOptions?.apply(this, arguments) || [];
options.push({
content: "🔄 Refresh Files",
callback: () => {
const subfolderWidget = this.widgets?.find(w => w.name === "subfolder");
const currentSubfolder = subfolderWidget?.value || "";
this.updateImageList(currentSubfolder);
}
});
return result;
};
}
}
});