Add nested subfolder support. Prevent selection reverting on refresh
This commit is contained in:
@@ -80,11 +80,11 @@ class SubfolderImageLoader:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"subfolder": (subfolders, {
|
"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."
|
"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 [""], {
|
"image": (default_images if default_images else [""], {
|
||||||
"default": default_images[0] if default_images else "",
|
"default": None,
|
||||||
"image_upload": True,
|
"image_upload": True,
|
||||||
"tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection."
|
"tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection."
|
||||||
}),
|
}),
|
||||||
@@ -100,58 +100,58 @@ class SubfolderImageLoader:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_images_for_subfolder(cls, subfolder: str = "") -> list:
|
def get_images_for_subfolder(cls, subfolder: str = "") -> list:
|
||||||
"""Get filtered images for a specific subfolder."""
|
"""Get filtered images for a specific subfolder."""
|
||||||
input_dir = folder_paths.get_input_directory()
|
all_images = cls.get_all_images_with_paths(folder_paths.get_input_directory())
|
||||||
all_images = cls.get_all_images_with_paths(input_dir)
|
|
||||||
|
if not subfolder:
|
||||||
if not subfolder or subfolder == "":
|
# Only images in root (no slash)
|
||||||
# Root folder - show only images without subfolder (no slash)
|
return [img for img in all_images if "/" not in img]
|
||||||
return [img for img in all_images if '/' not in img]
|
|
||||||
else:
|
prefix = subfolder.rstrip("/") + "/"
|
||||||
# Specific subfolder - show images WITH subfolder prefix for ComfyUI compatibility
|
return [
|
||||||
prefix = subfolder + "/"
|
img for img in all_images
|
||||||
filtered = [img for img in all_images
|
if img.startswith(prefix) and "/" not in img[len(prefix):]
|
||||||
if img.startswith(prefix) and '/' not in img[len(prefix):]]
|
]
|
||||||
return filtered
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_subfolders(cls, base_path: str) -> List[str]:
|
def get_subfolders(cls, base_path: str) -> List[str]:
|
||||||
"""Get list of subfolders in the base directory."""
|
"""Get list of subfolders in the base directory."""
|
||||||
|
subfolders = [""]
|
||||||
|
|
||||||
if not os.path.exists(base_path):
|
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('.')]
|
||||||
try:
|
rel_root = os.path.relpath(root, base_path)
|
||||||
for item in sorted(os.listdir(base_path)):
|
if rel_root != ".":
|
||||||
item_path = os.path.join(base_path, item)
|
subfolders.append(rel_root)
|
||||||
if os.path.isdir(item_path) and not item.startswith('.'):
|
|
||||||
subfolders.append(item)
|
return sorted(subfolders)
|
||||||
except PermissionError:
|
|
||||||
logging.warning(f"Permission denied accessing {base_path}")
|
|
||||||
|
|
||||||
return subfolders
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_images_with_paths(cls, base_path: str) -> List[str]:
|
def get_all_images_with_paths(cls, base_path: str) -> List[str]:
|
||||||
"""Get all images with their relative paths."""
|
"""Get all images with their relative paths."""
|
||||||
all_images = []
|
all_images = []
|
||||||
|
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif', '.gif'}
|
||||||
|
|
||||||
if not os.path.exists(base_path):
|
if not os.path.exists(base_path):
|
||||||
return all_images
|
return all_images
|
||||||
|
|
||||||
# Get images from root
|
for root, dirs, files in os.walk(base_path):
|
||||||
root_images = cls.get_images_from_folder(base_path)
|
# Skip hidden folders
|
||||||
all_images.extend(root_images)
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||||
|
|
||||||
# Get images from each subfolder with path prefix
|
rel_root = os.path.relpath(root, base_path)
|
||||||
for subfolder in cls.get_subfolders(base_path):
|
rel_root = "" if rel_root == "." else rel_root
|
||||||
if subfolder: # Skip empty root option
|
|
||||||
subfolder_path = os.path.join(base_path, subfolder)
|
for file in files:
|
||||||
images = cls.get_images_from_folder(subfolder_path)
|
ext = os.path.splitext(file.lower())[1]
|
||||||
# Add with subfolder prefix
|
if ext in valid_extensions:
|
||||||
for img in images:
|
if rel_root:
|
||||||
all_images.append(f"{subfolder}/{img}")
|
all_images.append(f"{rel_root}/{file}")
|
||||||
|
else:
|
||||||
|
all_images.append(file)
|
||||||
|
|
||||||
return sorted(all_images)
|
return sorted(all_images)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -185,13 +185,9 @@ class SubfolderImageLoader:
|
|||||||
actual_subfolder = subfolder
|
actual_subfolder = subfolder
|
||||||
|
|
||||||
# If image contains a path separator, extract the subfolder and filename
|
# If image contains a path separator, extract the subfolder and filename
|
||||||
if '/' in image:
|
if "/" in image:
|
||||||
parts = image.split('/')
|
parts = image.split("/", 1)
|
||||||
if len(parts) == 2:
|
actual_subfolder, clean_image = parts
|
||||||
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
|
# Build the full path
|
||||||
if actual_subfolder:
|
if actual_subfolder:
|
||||||
@@ -227,17 +223,20 @@ class SubfolderImageLoader:
|
|||||||
try:
|
try:
|
||||||
input_dir = folder_paths.get_input_directory()
|
input_dir = folder_paths.get_input_directory()
|
||||||
|
|
||||||
# Build path
|
# If image contains subfolder path
|
||||||
|
if "/" in image:
|
||||||
|
subfolder, image = image.split("/", 1)
|
||||||
|
|
||||||
if subfolder:
|
if subfolder:
|
||||||
file_path = os.path.join(input_dir, subfolder, image)
|
file_path = os.path.join(input_dir, subfolder, image)
|
||||||
else:
|
else:
|
||||||
file_path = os.path.join(input_dir, image)
|
file_path = os.path.join(input_dir, image)
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
return os.path.getmtime(file_path)
|
return os.path.getmtime(file_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT", "INT")
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT", "INT")
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ app.registerExtension({
|
|||||||
this._updatingImageList = false;
|
this._updatingImageList = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to preserve the current image selection
|
||||||
|
const previousValue =
|
||||||
|
imageWidget.value ??
|
||||||
|
this.properties?.image ??
|
||||||
|
"";
|
||||||
|
|
||||||
// Fetch updated image list from the backend
|
// Fetch updated image list from the backend
|
||||||
const response = await fetch("/subfolder_loader/refresh", {
|
const response = await fetch("/subfolder_loader/refresh", {
|
||||||
@@ -162,8 +168,15 @@ app.registerExtension({
|
|||||||
// Remove the old widget
|
// Remove the old widget
|
||||||
this.widgets.splice(imageIndex, 1);
|
this.widgets.splice(imageIndex, 1);
|
||||||
|
|
||||||
// Determine the new value - always use first image from filtered list
|
// Determine the new value:
|
||||||
const newValue = filteredImages.length > 0 ? filteredImages[0] : "";
|
// 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
|
// Create new widget with updated options
|
||||||
const newWidget = this.addWidget(
|
const newWidget = this.addWidget(
|
||||||
@@ -186,6 +199,9 @@ app.registerExtension({
|
|||||||
image_upload: true
|
image_upload: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ensure widget and node property reflect the preserved value
|
||||||
|
newWidget.value = newValue;
|
||||||
|
|
||||||
// Move the new widget to the correct position
|
// Move the new widget to the correct position
|
||||||
if (imageIndex < this.widgets.length - 1) {
|
if (imageIndex < this.widgets.length - 1) {
|
||||||
@@ -236,4 +252,4 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user