From 7752d56a032d36a284a361c257d18837999b129a Mon Sep 17 00:00:00 2001 From: rdomunky Date: Sat, 19 Jul 2025 21:21:16 -0700 Subject: [PATCH] Add image preview functionality and fix subfolder filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add image preview display matching standard LoadImage node behavior - Fix image path handling to use ComfyUI's folder_paths.get_annotated_filepath - Enable image_upload widget capability - Implement delayed update strategy to override ComfyUI's automatic image loading - Add race condition protection for image preview updates - Maintain subfolder prefix in image names for proper ComfyUI compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- subfolder_loader.py | 37 ++++-------- web/js/subfolder_image_loader.js | 99 ++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 30 deletions(-) diff --git a/subfolder_loader.py b/subfolder_loader.py index 9d9c876..93701fe 100644 --- a/subfolder_loader.py +++ b/subfolder_loader.py @@ -85,6 +85,7 @@ class SubfolderImageLoader: }), "image": (default_images if default_images else [""], { "default": default_images[0] if default_images else "", + "image_upload": True, "tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection." }), }, @@ -106,9 +107,9 @@ class SubfolderImageLoader: # 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 + # Specific subfolder - show images WITH subfolder prefix for ComfyUI compatibility prefix = subfolder + "/" - filtered = [img[len(prefix):] for img in all_images + filtered = [img for img in all_images if img.startswith(prefix) and '/' not in img[len(prefix):]] return filtered @@ -264,34 +265,20 @@ class SubfolderImageLoader: input_dir = folder_paths.get_input_directory() - # Handle the case where image might contain subfolder prefix - clean_image = image - actual_subfolder = subfolder + # The image parameter now contains the full path (subfolder/filename) when applicable + # Use it directly as the image identifier for ComfyUI + image_identifier = image - # If image contains a path separator, extract the subfolder and filename + # Extract clean filename for return value 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) + clean_image = parts[-1] # Get the filename part else: - file_path = os.path.join(input_dir, clean_image) + clean_image = 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}") + # Use ComfyUI's standard method to get the full file path + # This handles the path resolution and validation automatically + file_path = folder_paths.get_annotated_filepath(image_identifier) # Load and process image image_tensor, mask_tensor = self.process_image(file_path, load_mask) diff --git a/web/js/subfolder_image_loader.js b/web/js/subfolder_image_loader.js index 475c67d..00d2360 100644 --- a/web/js/subfolder_image_loader.js +++ b/web/js/subfolder_image_loader.js @@ -27,6 +27,74 @@ app.registerExtension({ this.updateImageList(value); }; + // Flag to prevent concurrent updateImageList calls + this._updatingImageList = false; + + // Track all pending image loads so we can cancel them + this._pendingImages = []; + + // Disable ComfyUI's automatic image loading to prevent interference + this.imageIndex = null; // Remove automatic image loading + + // Override onLoaded function to prevent automatic loading + this.onLoaded = function() { + // Do nothing - prevent ComfyUI's automatic image loading + }; + + // Simple image array for preview functionality + this.imgs = []; + + // showImage function with delayed updates to override ComfyUI's automatic loading + this.showImage = function(name) { + // Cancel ALL previous pending image loads + this._pendingImages.forEach(pendingImg => { + pendingImg.onload = null; + pendingImg.onerror = null; + pendingImg.src = ''; // Stop loading + }); + this._pendingImages = []; + + const img = new Image(); + + // Add to pending list + this._pendingImages.push(img); + + img.onload = () => { + // Only update if this image is still in the pending list (not cancelled) + if (this._pendingImages.includes(img)) { + // Apply multiple delayed updates to override ComfyUI's persistent automatic loading + [100, 200, 300].forEach(delay => { + setTimeout(() => { + // Check if this image is still the most recent one + if (this._pendingImages.includes(img)) { + this.imgs = [img]; + app.graph.setDirtyCanvas(true); + } + }, delay); + }); + + // Remove from pending list after all delays + setTimeout(() => { + const index = this._pendingImages.indexOf(img); + if (index > -1) { + this._pendingImages.splice(index, 1); + } + }, 350); + } + }; + + if (name) { + let subfolder = this.widgets?.find(w => w.name === "subfolder")?.value ?? ""; + // Extract filename from path if needed + const filename = name.includes('/') ? name.split('/').pop() : name; + const imageUrl = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); + img.src = imageUrl; + } else { + this.imgs = []; + app.graph.setDirtyCanvas(true); + } + }; + // Also run initial filtering based on current subfolder value const currentSubfolder = subfolderWidget.value || ""; this.updateImageList(currentSubfolder); @@ -41,10 +109,18 @@ app.registerExtension({ // Method to update image list based on selected subfolder nodeType.prototype.updateImageList = async function(selectedSubfolder) { + // Prevent concurrent updateImageList calls + if (this._updatingImageList) { + return; + } + try { + // Set flag to indicate we're updating the list programmatically + this._updatingImageList = true; // Get the image widget const imageWidget = this.widgets?.find(w => w.name === "image"); if (!imageWidget) { + this._updatingImageList = false; return; } @@ -62,12 +138,14 @@ app.registerExtension({ if (!response.ok) { console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText); + this._updatingImageList = false; return; } const data = await response.json(); if (!data.success) { console.error("SubfolderImageLoader: Server error:", data.error); + this._updatingImageList = false; return; } @@ -93,13 +171,19 @@ app.registerExtension({ "image", newValue, (value) => { + // Always show image preview when widget value changes + if (this.showImage) { + this.showImage(value); + } + // Call original callback if it existed if (oldCallback) { oldCallback.call(newWidget, value); } }, { - values: filteredImages.length > 0 ? filteredImages : [""] + values: filteredImages.length > 0 ? filteredImages : [""], + image_upload: true } ); @@ -112,10 +196,12 @@ app.registerExtension({ // 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); - } + // Use requestAnimationFrame for initial image display + requestAnimationFrame(() => { + if (newValue && this.showImage) { + this.showImage(newValue); + } + }); } // Force UI update @@ -126,6 +212,9 @@ app.registerExtension({ } catch (error) { console.error("SubfolderImageLoader: Error updating image list:", error); + } finally { + // Always clear the flag when done + this._updatingImageList = false; } };