From f73022020d7d3ecee687211493eb05594e8c4b65 Mon Sep 17 00:00:00 2001 From: Vaelek Date: Mon, 13 Apr 2026 21:31:57 -0500 Subject: [PATCH] Add nested subfolder support. Prevent selection reverting on refresh --- subfolder_loader.py | 103 +++++++++++++++---------------- web/js/subfolder_image_loader.js | 22 ++++++- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/subfolder_loader.py b/subfolder_loader.py index 93701fe..b45dac0 100644 --- a/subfolder_loader.py +++ b/subfolder_loader.py @@ -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,58 +100,58 @@ 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) - - 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 + all_images = cls.get_all_images_with_paths(folder_paths.get_input_directory()) + + 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 [""] - - 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 + return subfolders + + 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) + + 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) - - # 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}") - + + for root, dirs, files in os.walk(base_path): + # Skip hidden folders + dirs[:] = [d for d in dirs if not d.startswith('.')] + + 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) @classmethod @@ -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,17 +223,20 @@ 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: 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") diff --git a/web/js/subfolder_image_loader.js b/web/js/subfolder_image_loader.js index 00d2360..fcdee4e 100644 --- a/web/js/subfolder_image_loader.js +++ b/web/js/subfolder_image_loader.js @@ -123,6 +123,12 @@ app.registerExtension({ this._updatingImageList = false; 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", { @@ -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( @@ -186,6 +199,9 @@ app.registerExtension({ image_upload: true } ); + + // 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) { @@ -236,4 +252,4 @@ app.registerExtension({ }; } } -}); \ No newline at end of file +});