Add image preview functionality and fix subfolder filtering

- 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 <noreply@anthropic.com>
This commit is contained in:
rdomunky
2025-07-19 21:21:16 -07:00
parent 0de6ee8b12
commit 7752d56a03
2 changed files with 106 additions and 30 deletions

View File

@@ -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)

View File

@@ -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;
}
};