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:
@@ -85,6 +85,7 @@ class SubfolderImageLoader:
|
|||||||
}),
|
}),
|
||||||
"image": (default_images if default_images else [""], {
|
"image": (default_images if default_images else [""], {
|
||||||
"default": default_images[0] 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."
|
"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)
|
# 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:
|
else:
|
||||||
# Specific subfolder - show only images from that subfolder, without prefix
|
# Specific subfolder - show images WITH subfolder prefix for ComfyUI compatibility
|
||||||
prefix = subfolder + "/"
|
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):]]
|
if img.startswith(prefix) and '/' not in img[len(prefix):]]
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
@@ -264,34 +265,20 @@ class SubfolderImageLoader:
|
|||||||
|
|
||||||
input_dir = folder_paths.get_input_directory()
|
input_dir = folder_paths.get_input_directory()
|
||||||
|
|
||||||
# Handle the case where image might contain subfolder prefix
|
# The image parameter now contains the full path (subfolder/filename) when applicable
|
||||||
clean_image = image
|
# Use it directly as the image identifier for ComfyUI
|
||||||
actual_subfolder = subfolder
|
image_identifier = image
|
||||||
|
|
||||||
# If image contains a path separator, extract the subfolder and filename
|
# Extract clean filename for return value
|
||||||
if '/' in image:
|
if '/' in image:
|
||||||
parts = image.split('/')
|
parts = image.split('/')
|
||||||
if len(parts) == 2:
|
clean_image = parts[-1] # Get the filename part
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
file_path = os.path.join(input_dir, clean_image)
|
clean_image = image
|
||||||
|
|
||||||
# Validate path is safe
|
# Use ComfyUI's standard method to get the full file path
|
||||||
file_path_abs = os.path.abspath(file_path)
|
# This handles the path resolution and validation automatically
|
||||||
input_dir_abs = os.path.abspath(input_dir)
|
file_path = folder_paths.get_annotated_filepath(image_identifier)
|
||||||
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}")
|
|
||||||
|
|
||||||
# Load and process image
|
# Load and process image
|
||||||
image_tensor, mask_tensor = self.process_image(file_path, load_mask)
|
image_tensor, mask_tensor = self.process_image(file_path, load_mask)
|
||||||
|
|||||||
@@ -27,6 +27,74 @@ app.registerExtension({
|
|||||||
this.updateImageList(value);
|
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
|
// Also run initial filtering based on current subfolder value
|
||||||
const currentSubfolder = subfolderWidget.value || "";
|
const currentSubfolder = subfolderWidget.value || "";
|
||||||
this.updateImageList(currentSubfolder);
|
this.updateImageList(currentSubfolder);
|
||||||
@@ -41,10 +109,18 @@ app.registerExtension({
|
|||||||
|
|
||||||
// Method to update image list based on selected subfolder
|
// Method to update image list based on selected subfolder
|
||||||
nodeType.prototype.updateImageList = async function(selectedSubfolder) {
|
nodeType.prototype.updateImageList = async function(selectedSubfolder) {
|
||||||
|
// Prevent concurrent updateImageList calls
|
||||||
|
if (this._updatingImageList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Set flag to indicate we're updating the list programmatically
|
||||||
|
this._updatingImageList = true;
|
||||||
// Get the image widget
|
// Get the image widget
|
||||||
const imageWidget = this.widgets?.find(w => w.name === "image");
|
const imageWidget = this.widgets?.find(w => w.name === "image");
|
||||||
if (!imageWidget) {
|
if (!imageWidget) {
|
||||||
|
this._updatingImageList = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +138,14 @@ app.registerExtension({
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText);
|
console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText);
|
||||||
|
this._updatingImageList = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
console.error("SubfolderImageLoader: Server error:", data.error);
|
console.error("SubfolderImageLoader: Server error:", data.error);
|
||||||
|
this._updatingImageList = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,13 +171,19 @@ app.registerExtension({
|
|||||||
"image",
|
"image",
|
||||||
newValue,
|
newValue,
|
||||||
(value) => {
|
(value) => {
|
||||||
|
// Always show image preview when widget value changes
|
||||||
|
if (this.showImage) {
|
||||||
|
this.showImage(value);
|
||||||
|
}
|
||||||
|
|
||||||
// Call original callback if it existed
|
// Call original callback if it existed
|
||||||
if (oldCallback) {
|
if (oldCallback) {
|
||||||
oldCallback.call(newWidget, value);
|
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
|
// Force update the node's properties
|
||||||
this.setProperty("image", newValue);
|
this.setProperty("image", newValue);
|
||||||
|
|
||||||
// Trigger the widget callback to ensure the change is registered
|
// Use requestAnimationFrame for initial image display
|
||||||
if (newWidget.callback) {
|
requestAnimationFrame(() => {
|
||||||
newWidget.callback(newValue);
|
if (newValue && this.showImage) {
|
||||||
}
|
this.showImage(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force UI update
|
// Force UI update
|
||||||
@@ -126,6 +212,9 @@ app.registerExtension({
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("SubfolderImageLoader: Error updating image list:", error);
|
console.error("SubfolderImageLoader: Error updating image list:", error);
|
||||||
|
} finally {
|
||||||
|
// Always clear the flag when done
|
||||||
|
this._updatingImageList = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user