Add nested subfolder support. Prevent selection reverting on refresh

This commit is contained in:
Vaelek
2026-04-13 21:31:57 -05:00
parent 7752d56a03
commit f73022020d
2 changed files with 70 additions and 55 deletions

View File

@@ -80,11 +80,11 @@ class SubfolderImageLoader:
return {
"required": {
"subfolder": (subfolders, {
"default": subfolders[0] if subfolders else "",
"default": None,
"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 "",
"default": None,
"image_upload": True,
"tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection."
}),
@@ -100,57 +100,57 @@ class SubfolderImageLoader:
@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)
all_images = cls.get_all_images_with_paths(folder_paths.get_input_directory())
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 images WITH subfolder prefix for ComfyUI compatibility
prefix = subfolder + "/"
filtered = [img for img in all_images
if img.startswith(prefix) and '/' not in img[len(prefix):]]
return filtered
if not subfolder:
# Only images in root (no slash)
return [img for img in all_images if "/" not in img]
prefix = subfolder.rstrip("/") + "/"
return [
img for img in all_images
if img.startswith(prefix) and "/" not in img[len(prefix):]
]
@classmethod
def get_subfolders(cls, base_path: str) -> List[str]:
"""Get list of subfolders in the base directory."""
subfolders = [""]
if not os.path.exists(base_path):
return [""]
return subfolders
subfolders = [""] # Include root/no subfolder option
for root, dirs, _ in os.walk(base_path):
dirs[:] = [d for d in dirs if not d.startswith('.')]
rel_root = os.path.relpath(root, base_path)
if rel_root != ".":
subfolders.append(rel_root)
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
return sorted(subfolders)
@classmethod
def get_all_images_with_paths(cls, base_path: str) -> List[str]:
"""Get all images with their relative paths."""
all_images = []
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif', '.gif'}
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)
for root, dirs, files in os.walk(base_path):
# Skip hidden folders
dirs[:] = [d for d in dirs if not d.startswith('.')]
# 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}")
rel_root = os.path.relpath(root, base_path)
rel_root = "" if rel_root == "." else rel_root
for file in files:
ext = os.path.splitext(file.lower())[1]
if ext in valid_extensions:
if rel_root:
all_images.append(f"{rel_root}/{file}")
else:
all_images.append(file)
return sorted(all_images)
@@ -185,13 +185,9 @@ class SubfolderImageLoader:
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
if "/" in image:
parts = image.split("/", 1)
actual_subfolder, clean_image = parts
# Build the full path
if actual_subfolder:
@@ -227,7 +223,10 @@ class SubfolderImageLoader:
try:
input_dir = folder_paths.get_input_directory()
# Build path
# If image contains subfolder path
if "/" in image:
subfolder, image = image.split("/", 1)
if subfolder:
file_path = os.path.join(input_dir, subfolder, image)
else:

View File

@@ -124,6 +124,12 @@ app.registerExtension({
return;
}
// Try to preserve the current image selection
const previousValue =
imageWidget.value ??
this.properties?.image ??
"";
// Fetch updated image list from the backend
const response = await fetch("/subfolder_loader/refresh", {
method: "POST",
@@ -162,8 +168,15 @@ app.registerExtension({
// 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] : "";
// Determine the new value:
// 1. If previous value is still valid, keep it
// 2. Otherwise, fall back to first filtered image
let newValue = "";
if (previousValue && filteredImages.includes(previousValue)) {
newValue = previousValue;
} else if (filteredImages.length > 0) {
newValue = filteredImages[0];
}
// Create new widget with updated options
const newWidget = this.addWidget(
@@ -187,6 +200,9 @@ app.registerExtension({
}
);
// Ensure widget and node property reflect the preserved value
newWidget.value = newValue;
// Move the new widget to the correct position
if (imageIndex < this.widgets.length - 1) {
const widget = this.widgets.pop();