fix(web): escape server data in innerHTML and plug listener leaks
- Escape file names, paths and image metadata interpolated into innerHTML in the local image loader and save image viewer (XSS via crafted file names or a shared workflow's filename_prefix) - Save image viewer: bind document drag listeners once and remove them in disconnectedCallback instead of adding two per run; drop debug logging and the window.kikoSaveImageTest helper - Local image loader: check response.ok and surface 400/403 errors, disconnect the ResizeObserver on node removal - Gemini prompt: restore the user's node color after the run flash - Workflow timer: chain onRemoved, merge properties in onConfigure - Seed history: build entries with textContent
This commit is contained in:
+25
-4
@@ -138,8 +138,16 @@ app.registerExtension({
|
||||
const result = onExecuted?.apply(this, arguments);
|
||||
|
||||
if (this.status) {
|
||||
// Remember the user's color only if no flash is already pending,
|
||||
// otherwise we would capture our own temporary flash color
|
||||
if (this.geminiFlashTimeout) {
|
||||
clearTimeout(this.geminiFlashTimeout);
|
||||
} else {
|
||||
this.geminiPrevBgcolor = this.bgcolor;
|
||||
}
|
||||
|
||||
// Check if there was an error in the output
|
||||
const outputs = message.output;
|
||||
const outputs = message?.output;
|
||||
if (outputs && outputs.prompt && outputs.prompt[0] && outputs.prompt[0].startsWith("Error:")) {
|
||||
this.status.value = "Error - Check output";
|
||||
this.bgcolor = "#552222";
|
||||
@@ -148,17 +156,30 @@ app.registerExtension({
|
||||
this.bgcolor = "#225522";
|
||||
}
|
||||
|
||||
// Reset color after delay
|
||||
setTimeout(() => {
|
||||
this.bgcolor = "";
|
||||
// Restore the previous color after delay
|
||||
this.geminiFlashTimeout = setTimeout(() => {
|
||||
this.geminiFlashTimeout = null;
|
||||
this.bgcolor = this.geminiPrevBgcolor;
|
||||
this.geminiPrevBgcolor = undefined;
|
||||
if (this.status) {
|
||||
this.status.value = "Ready";
|
||||
}
|
||||
this.setDirtyCanvas?.(true, true);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Clear any pending color-restore timer when the node is removed
|
||||
const onRemoved = nodeType.prototype.onRemoved;
|
||||
nodeType.prototype.onRemoved = function() {
|
||||
if (this.geminiFlashTimeout) {
|
||||
clearTimeout(this.geminiFlashTimeout);
|
||||
this.geminiFlashTimeout = null;
|
||||
}
|
||||
return onRemoved?.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
|
||||
// Escape a value for safe interpolation into HTML text or attribute contexts
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Build a readable error message from a non-OK fetch response
|
||||
async function describeHttpError(response) {
|
||||
let detail = '';
|
||||
try {
|
||||
const body = await response.json();
|
||||
detail = body?.error || body?.message || '';
|
||||
} catch (e) {
|
||||
// Non-JSON body; fall back to the status code
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return detail || 'Access denied: path is outside the allowed folders';
|
||||
}
|
||||
if (response.status === 400) {
|
||||
return detail || 'Invalid path or request';
|
||||
}
|
||||
return detail || `Request failed (HTTP ${response.status})`;
|
||||
}
|
||||
|
||||
// Setup global lightbox for image preview
|
||||
function setupGlobalLightbox() {
|
||||
if (document.getElementById('kiko-image-lightbox')) return;
|
||||
@@ -511,7 +539,22 @@ app.registerExtension({
|
||||
const debouncedLayout = debounce(applyMasonryLayout, 20);
|
||||
|
||||
// Set up ResizeObserver for responsive layout
|
||||
new ResizeObserver(debouncedLayout).observe(cardHolder);
|
||||
this.kikoResizeObserver = new ResizeObserver(debouncedLayout);
|
||||
this.kikoResizeObserver.observe(cardHolder);
|
||||
|
||||
const originalOnRemoved = this.onRemoved;
|
||||
this.onRemoved = function () {
|
||||
if (this.kikoResizeObserver) {
|
||||
this.kikoResizeObserver.disconnect();
|
||||
this.kikoResizeObserver = null;
|
||||
}
|
||||
return originalOnRemoved?.apply(this, arguments);
|
||||
};
|
||||
|
||||
const showStatus = (message) => {
|
||||
statusMessage.style.display = 'block';
|
||||
statusMessage.textContent = message;
|
||||
};
|
||||
|
||||
// Autocomplete functionality
|
||||
let autocompleteSelectedIndex = -1;
|
||||
@@ -521,6 +564,10 @@ app.registerExtension({
|
||||
async function fetchDirectorySuggestions(path) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/kiko_local_image_loader/list_directories?path=${encodeURIComponent(path)}`);
|
||||
if (!response.ok) {
|
||||
// Disallowed or invalid paths simply yield no suggestions
|
||||
return [];
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.directories || [];
|
||||
} catch (error) {
|
||||
@@ -545,11 +592,11 @@ app.registerExtension({
|
||||
const parentPath = parts.length > 0 ? '/' + parts.join('/') : '';
|
||||
|
||||
return `
|
||||
<div class="kiko-autocomplete-item" data-index="${index}" data-path="${path}">
|
||||
<div class="kiko-autocomplete-item" data-index="${index}" data-path="${escapeHtml(path)}">
|
||||
<div class="folder-icon">${smallFolderSVG}</div>
|
||||
<div class="path-text">
|
||||
<span class="folder-name">${folderName}</span>
|
||||
${parentPath ? `<span class="parent-path"> — ${parentPath}</span>` : ''}
|
||||
<span class="folder-name">${escapeHtml(folderName)}</span>
|
||||
${parentPath ? `<span class="parent-path"> — ${escapeHtml(parentPath)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -668,6 +715,9 @@ app.registerExtension({
|
||||
async function loadSavedPaths() {
|
||||
try {
|
||||
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
|
||||
if (!response.ok) {
|
||||
throw new Error(await describeHttpError(response));
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
pathPresets.innerHTML = '<option value="">-- Saved Paths --</option>';
|
||||
@@ -689,20 +739,27 @@ app.registerExtension({
|
||||
|
||||
try {
|
||||
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
|
||||
if (!response.ok) {
|
||||
throw new Error(await describeHttpError(response));
|
||||
}
|
||||
const data = await response.json();
|
||||
const savedPaths = data.saved_paths || [];
|
||||
|
||||
if (!savedPaths.includes(currentDirectory)) {
|
||||
savedPaths.push(currentDirectory);
|
||||
await api.fetchApi('/kiko_local_image_loader/save_paths', {
|
||||
const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: savedPaths })
|
||||
});
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error(await describeHttpError(saveResponse));
|
||||
}
|
||||
await loadSavedPaths();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving path:', error);
|
||||
showStatus(`Error saving path: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -713,20 +770,27 @@ app.registerExtension({
|
||||
|
||||
try {
|
||||
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
|
||||
if (!response.ok) {
|
||||
throw new Error(await describeHttpError(response));
|
||||
}
|
||||
const data = await response.json();
|
||||
const savedPaths = data.saved_paths || [];
|
||||
|
||||
const updatedPaths = savedPaths.filter(p => p !== selectedPath);
|
||||
if (updatedPaths.length !== savedPaths.length) {
|
||||
await api.fetchApi('/kiko_local_image_loader/save_paths', {
|
||||
const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: updatedPaths })
|
||||
});
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error(await describeHttpError(saveResponse));
|
||||
}
|
||||
await loadSavedPaths();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting path:', error);
|
||||
showStatus(`Error deleting path: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -757,6 +821,10 @@ app.registerExtension({
|
||||
|
||||
try {
|
||||
const response = await api.fetchApi(`/kiko_local_image_loader/images?${params}`);
|
||||
if (!response.ok) {
|
||||
statusMessage.textContent = await describeHttpError(response);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
@@ -820,7 +888,7 @@ app.registerExtension({
|
||||
<div class="kiko-card-media-wrapper">
|
||||
<div class="kiko-folder-card">
|
||||
<div class="kiko-folder-icon">${folderSVG}</div>
|
||||
<div class="kiko-folder-name">${item.name}</div>
|
||||
<div class="kiko-folder-name">${escapeHtml(item.name)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -831,9 +899,9 @@ app.registerExtension({
|
||||
const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
|
||||
card.innerHTML = `
|
||||
<div class="kiko-card-media-wrapper">
|
||||
<img src="${thumbnailUrl}" alt="${item.name}" />
|
||||
<img src="${escapeHtml(thumbnailUrl)}" alt="${escapeHtml(item.name)}" />
|
||||
</div>
|
||||
<div class="kiko-card-info-panel">${item.name}</div>
|
||||
<div class="kiko-card-info-panel">${escapeHtml(item.name)}</div>
|
||||
`;
|
||||
|
||||
// Trigger layout when image loads
|
||||
@@ -856,10 +924,10 @@ app.registerExtension({
|
||||
const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
|
||||
card.innerHTML = `
|
||||
<div class="kiko-card-media-wrapper">
|
||||
<img src="${thumbnailUrl}" alt="${item.name}" />
|
||||
<img src="${escapeHtml(thumbnailUrl)}" alt="${escapeHtml(item.name)}" />
|
||||
<div class="kiko-video-card-overlay">${videoSVG}</div>
|
||||
</div>
|
||||
<div class="kiko-card-info-panel">${item.name}</div>
|
||||
<div class="kiko-card-info-panel">${escapeHtml(item.name)}</div>
|
||||
`;
|
||||
|
||||
const img = card.querySelector('img');
|
||||
@@ -882,7 +950,7 @@ app.registerExtension({
|
||||
<div class="kiko-card-media-wrapper">
|
||||
<div class="kiko-audio-card">
|
||||
<div class="kiko-audio-icon">${audioSVG}</div>
|
||||
<div class="kiko-audio-name">${item.name}</div>
|
||||
<div class="kiko-audio-name">${escapeHtml(item.name)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -924,7 +992,7 @@ app.registerExtension({
|
||||
|
||||
// Send to backend
|
||||
try {
|
||||
await api.fetchApi('/kiko_local_image_loader/set_node_selection', {
|
||||
const response = await api.fetchApi('/kiko_local_image_loader/set_node_selection', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -933,11 +1001,16 @@ app.registerExtension({
|
||||
type: type
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
showStatus(`Selection rejected: ${await describeHttpError(response)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger node update
|
||||
node.setDirtyCanvas(true);
|
||||
} catch (error) {
|
||||
console.error('Error setting selection:', error);
|
||||
showStatus('Error setting selection');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,10 +1070,13 @@ app.registerExtension({
|
||||
|
||||
// Try to load last path
|
||||
api.fetchApi('/kiko_local_image_loader/get_last_path').then(async response => {
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
if (data.last_path) {
|
||||
loadImages(data.last_path);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading last path:', error);
|
||||
});
|
||||
|
||||
return r;
|
||||
|
||||
+49
-86
@@ -377,6 +377,26 @@ function injectStyles() {
|
||||
}
|
||||
}
|
||||
|
||||
// Escape a value for safe interpolation into HTML text or attribute contexts
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Build a ComfyUI /api/view URL with every parameter encoded
|
||||
function buildViewUrl(filename, subfolder, type) {
|
||||
const params = new URLSearchParams({ filename: filename ?? '' });
|
||||
if (subfolder && subfolder.trim()) {
|
||||
params.set('subfolder', subfolder);
|
||||
}
|
||||
params.set('type', type || 'output');
|
||||
return `${window.location.origin}/api/view?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Format file size for display
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
@@ -388,8 +408,6 @@ function formatFileSize(bytes) {
|
||||
|
||||
// Open image in new tab
|
||||
function openImageInTab(imagePath, subfolder = '', enablePopup = true) {
|
||||
console.log(`KikoSaveImage: Attempting to open image: ${imagePath}, subfolder: ${subfolder}`);
|
||||
|
||||
// Note: enablePopup parameter is kept for compatibility but not used
|
||||
// since popup now controls viewer visibility, not individual image clicks
|
||||
|
||||
@@ -403,16 +421,12 @@ function openImageInTab(imagePath, subfolder = '', enablePopup = true) {
|
||||
fullPath = `${basePath}/api/view?filename=${encodeURIComponent(imagePath)}&type=output`;
|
||||
}
|
||||
|
||||
console.log(`KikoSaveImage: Opening URL: ${fullPath}`);
|
||||
|
||||
// Extract clean filename for window name (remove timestamp and batch number)
|
||||
const cleanName = imagePath.split('_').slice(0, -2).join('_') || 'KikoSaveImage';
|
||||
|
||||
// Try to open the image with a clean window name
|
||||
const newWindow = window.open(fullPath, cleanName.replace(/[^a-zA-Z0-9]/g, '_'));
|
||||
if (newWindow) {
|
||||
console.log('KikoSaveImage: Successfully opened new window');
|
||||
} else {
|
||||
if (!newWindow) {
|
||||
console.error('KikoSaveImage: Failed to open new window - popup blocked?');
|
||||
// Fallback: copy URL to clipboard and alert user
|
||||
navigator.clipboard.writeText(fullPath).then(() => {
|
||||
@@ -435,6 +449,10 @@ class KikoImageViewer extends HTMLElement {
|
||||
this.dragOffset = { x: 0, y: 0 };
|
||||
this.lastHeaderClick = 0;
|
||||
|
||||
// Bind document-level drag handlers once so they can be removed later
|
||||
this.boundHandleDrag = this.handleDrag.bind(this);
|
||||
this.boundEndDrag = this.endDrag.bind(this);
|
||||
|
||||
// Always start at default position
|
||||
}
|
||||
|
||||
@@ -442,6 +460,16 @@ class KikoImageViewer extends HTMLElement {
|
||||
return ['data'];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
document.addEventListener('mousemove', this.boundHandleDrag);
|
||||
document.addEventListener('mouseup', this.boundEndDrag);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
document.removeEventListener('mousemove', this.boundHandleDrag);
|
||||
document.removeEventListener('mouseup', this.boundEndDrag);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name === 'data' && newValue) {
|
||||
try {
|
||||
@@ -455,8 +483,6 @@ class KikoImageViewer extends HTMLElement {
|
||||
}
|
||||
|
||||
setImageData(data) {
|
||||
console.log('KikoImageViewer: setImageData called with:', data);
|
||||
console.log('KikoImageViewer: First image data:', data[0]);
|
||||
this.imageData = data;
|
||||
this.autoUnrollOnNewImages();
|
||||
this.render();
|
||||
@@ -513,9 +539,7 @@ class KikoImageViewer extends HTMLElement {
|
||||
});
|
||||
});
|
||||
|
||||
// Global mouse events for dragging
|
||||
document.addEventListener('mousemove', this.handleDrag.bind(this));
|
||||
document.addEventListener('mouseup', this.endDrag.bind(this));
|
||||
// Document-level drag listeners are managed in connectedCallback/disconnectedCallback
|
||||
}
|
||||
|
||||
startDrag(e) {
|
||||
@@ -697,12 +721,9 @@ class KikoImageViewer extends HTMLElement {
|
||||
openAllImagesWithDelay() {
|
||||
const selectedIndices = Array.from(this.selectedImages);
|
||||
if (selectedIndices.length === 0) {
|
||||
console.log('KikoSaveImage: No images selected for opening');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`KikoSaveImage: Opening ${selectedIndices.length} images with delay`);
|
||||
|
||||
// Open images with 150ms delay between each to prevent popup blocking
|
||||
selectedIndices.forEach((index, i) => {
|
||||
setTimeout(() => {
|
||||
@@ -716,12 +737,7 @@ class KikoImageViewer extends HTMLElement {
|
||||
|
||||
downloadImage(imageData) {
|
||||
// Construct proper image URL handling subfolder
|
||||
let imageUrl;
|
||||
if (imageData.subfolder && imageData.subfolder.trim()) {
|
||||
imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&subfolder=${encodeURIComponent(imageData.subfolder)}&type=${imageData.type}`;
|
||||
} else {
|
||||
imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&type=${imageData.type}`;
|
||||
}
|
||||
const imageUrl = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type);
|
||||
|
||||
// Create temporary link and trigger download
|
||||
const link = document.createElement('a');
|
||||
@@ -748,7 +764,7 @@ class KikoImageViewer extends HTMLElement {
|
||||
}
|
||||
|
||||
const header = this.imageData.length === 1
|
||||
? `Saved Image (${this.imageData[0].format})`
|
||||
? `Saved Image (${escapeHtml(this.imageData[0].format)})`
|
||||
: `Saved Images (${this.imageData.length} files)`;
|
||||
|
||||
const containerStyle = this.isMinimized ? 'style="display: none;"' : '';
|
||||
@@ -791,15 +807,10 @@ class KikoImageViewer extends HTMLElement {
|
||||
|
||||
createImageItem(data, index) {
|
||||
// Construct proper image URL handling subfolder
|
||||
let imageUrl;
|
||||
if (data.subfolder && data.subfolder.trim()) {
|
||||
// ComfyUI expects subfolder to be passed separately
|
||||
imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&subfolder=${encodeURIComponent(data.subfolder)}&type=${data.type}`;
|
||||
} else {
|
||||
imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}`;
|
||||
}
|
||||
// ComfyUI expects subfolder to be passed separately
|
||||
const imageUrl = buildViewUrl(data.filename, data.subfolder, data.type);
|
||||
|
||||
const formatClass = `kiko-format-${data.format.toLowerCase()}`;
|
||||
const formatClass = `kiko-format-${String(data.format ?? '').toLowerCase().replace(/[^a-z0-9_-]/g, '')}`;
|
||||
|
||||
// Format file size
|
||||
const fileSize = data.file_size ? formatFileSize(data.file_size) : 'Unknown';
|
||||
@@ -818,7 +829,7 @@ class KikoImageViewer extends HTMLElement {
|
||||
|
||||
return `
|
||||
<div class="kiko-image-item" data-index="${index}">
|
||||
<img src="${imageUrl}" alt="${data.filename}" loading="lazy" />
|
||||
<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(data.filename)}" loading="lazy" />
|
||||
|
||||
${this.imageData.length > 1 ? `
|
||||
<div class="kiko-image-selector" data-index="${index}" title="Select image"></div>
|
||||
@@ -829,14 +840,14 @@ class KikoImageViewer extends HTMLElement {
|
||||
</div>
|
||||
|
||||
<div class="kiko-image-info">
|
||||
<div class="kiko-image-filename" title="${data.filename}">
|
||||
${data.filename}
|
||||
<div class="kiko-image-filename" title="${escapeHtml(data.filename)}">
|
||||
${escapeHtml(data.filename)}
|
||||
</div>
|
||||
<div class="kiko-image-details">
|
||||
<div class="kiko-image-stats">
|
||||
<span class="kiko-format-badge ${formatClass}">${data.format}</span>
|
||||
${data.dimensions || 'Unknown'} • ${fileSize}
|
||||
${qualityInfo ? ` • ${qualityInfo}` : ''}
|
||||
<span class="kiko-format-badge ${formatClass}">${escapeHtml(data.format)}</span>
|
||||
${escapeHtml(data.dimensions || 'Unknown')} • ${escapeHtml(fileSize)}
|
||||
${qualityInfo ? ` • ${escapeHtml(qualityInfo)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -852,7 +863,6 @@ class KikoImageViewer extends HTMLElement {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log(`KikoImageViewer: Clicking image ${index}: ${data.filename}`);
|
||||
openImageInTab(data.filename, data.subfolder || '');
|
||||
});
|
||||
}
|
||||
@@ -872,8 +882,7 @@ function createImagePreview(imageData) {
|
||||
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
const imagePath = `/api/view?filename=${imageData.filename}&type=${imageData.type}`;
|
||||
img.src = imagePath;
|
||||
img.src = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type);
|
||||
img.alt = imageData.filename;
|
||||
|
||||
// Create info overlay
|
||||
@@ -1012,17 +1021,13 @@ app.registerExtension({
|
||||
// Replace the standard image viewer with our custom web component
|
||||
const originalOnExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
console.log('KikoSaveImage: onExecuted called with message:', message);
|
||||
|
||||
// Skip the original ComfyUI preview system
|
||||
// Don't call originalOnExecuted to prevent default image display
|
||||
|
||||
// Use our custom web component instead
|
||||
if (message && message.kiko_enhanced && message.kiko_enhanced.length > 0) {
|
||||
console.log('KikoSaveImage: Creating custom image viewer');
|
||||
this.createCustomImageViewer(message.kiko_enhanced);
|
||||
} else {
|
||||
console.log('KikoSaveImage: No enhanced data found, falling back to standard preview');
|
||||
// Fall back to standard behavior if no enhanced data
|
||||
if (originalOnExecuted) {
|
||||
originalOnExecuted.apply(this, arguments);
|
||||
@@ -1032,14 +1037,10 @@ app.registerExtension({
|
||||
|
||||
// Add method to create custom image viewer
|
||||
nodeType.prototype.createCustomImageViewer = function(imageData) {
|
||||
console.log('KikoSaveImage: Creating custom viewer for', imageData.length, 'images');
|
||||
|
||||
// Check if popup is enabled for any image (use first image's popup setting)
|
||||
const popupEnabled = imageData.length > 0 ? imageData[0].popup : true;
|
||||
console.log('KikoSaveImage: Popup enabled:', popupEnabled);
|
||||
|
||||
if (!popupEnabled) {
|
||||
console.log('KikoSaveImage: Popup disabled, not showing custom viewer');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1049,7 +1050,6 @@ app.registerExtension({
|
||||
// Update existing viewer with new data
|
||||
existingViewer.setImageData(imageData);
|
||||
existingViewer.show(); // Make sure it's visible
|
||||
console.log('KikoSaveImage: Updated existing viewer');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1072,8 +1072,6 @@ app.registerExtension({
|
||||
|
||||
// Append to body (floating window) - always visible
|
||||
document.body.appendChild(viewer);
|
||||
|
||||
console.log('KikoSaveImage: Custom viewer created successfully');
|
||||
};
|
||||
|
||||
// Add method to create toggle button
|
||||
@@ -1137,50 +1135,16 @@ app.registerExtension({
|
||||
|
||||
nodeElement.appendChild(toggleButton);
|
||||
this.kikoToggleButton = toggleButton;
|
||||
|
||||
console.log('KikoSaveImage: Toggle button created');
|
||||
};
|
||||
|
||||
// Legacy method kept for compatibility (not used with web component)
|
||||
nodeType.prototype.enhanceImagePreviews = function(imageData) {
|
||||
console.log('KikoSaveImage: Legacy enhanceImagePreviews called (should use web component instead)');
|
||||
// No-op: superseded by the kiko-image-viewer web component
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Export helper functions to global scope for debugging
|
||||
window.kikoSaveImageTest = function() {
|
||||
console.log('KikoSaveImage: Testing click functionality...');
|
||||
|
||||
// Find all images in the document
|
||||
const allImages = document.querySelectorAll('img');
|
||||
console.log(`Found ${allImages.length} images in document`);
|
||||
|
||||
// Try to find images that look like our saved images
|
||||
allImages.forEach((img, index) => {
|
||||
console.log(`Image ${index}: src = ${img.src}`);
|
||||
|
||||
// Add test click handler to all images
|
||||
img.style.border = '2px solid red';
|
||||
img.style.cursor = 'pointer';
|
||||
img.title = 'TEST: Click to open in new tab';
|
||||
|
||||
// Remove old handlers and add new one
|
||||
const newImg = img.cloneNode(true);
|
||||
img.parentNode.replaceChild(newImg, img);
|
||||
|
||||
newImg.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('TEST CLICK WORKED!', newImg.src);
|
||||
window.open(newImg.src, '_blank');
|
||||
}, true);
|
||||
});
|
||||
|
||||
console.log('KikoSaveImage: Test setup complete. All images should now be clickable with red borders.');
|
||||
};
|
||||
|
||||
// 🎉 RESET VIEWER WINDOWS 🎉
|
||||
window.kikoResetViewer = function() {
|
||||
console.log('🎉 Resetting KikoSaveImage viewer windows...');
|
||||
@@ -1219,4 +1183,3 @@ console.log('🎉 KikoSaveImage: Extension loaded!');
|
||||
console.log('💡 Helper functions available:');
|
||||
console.log(' 📞 kikoResetViewer() - Remove existing viewer windows');
|
||||
console.log(' 🚀 kikoForceViewer() - Show demo viewer');
|
||||
console.log(' 🧪 kikoSaveImageTest() - Test image click functionality');
|
||||
|
||||
@@ -448,8 +448,10 @@ const KikoWorkflowTimerExtension = {
|
||||
KikoGlobalTimer.registerNode(this);
|
||||
};
|
||||
|
||||
const origOnRemoved = nodeType.prototype.onRemoved;
|
||||
nodeType.prototype.onRemoved = function() {
|
||||
KikoGlobalTimer.unregisterNode(this);
|
||||
return origOnRemoved?.apply(this, arguments);
|
||||
};
|
||||
|
||||
const origOnSerialize = nodeType.prototype.onSerialize;
|
||||
@@ -461,7 +463,7 @@ const KikoWorkflowTimerExtension = {
|
||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function(info) {
|
||||
origOnConfigure?.apply(this, arguments);
|
||||
this.properties = info.properties || {};
|
||||
this.properties = { ...(this.properties || {}), ...(info?.properties || {}) };
|
||||
|
||||
if (this.timerDisplay) {
|
||||
this.timerDisplay.textContent = this.properties.elapsed_time_str || "00:00:000";
|
||||
|
||||
@@ -371,14 +371,15 @@ app.registerExtension({
|
||||
});
|
||||
|
||||
const timeAgo = this.formatTimeAgo(item.timestamp);
|
||||
entryDiv.innerHTML = `
|
||||
<div style="color: #fff; font-weight: bold; margin-bottom: 1px;">
|
||||
🎲 ${item.seed}
|
||||
</div>
|
||||
<div style="color: #999; font-size: 8px;">
|
||||
⏰ ${timeAgo}
|
||||
</div>
|
||||
`;
|
||||
// Build with textContent: history comes from localStorage and is untrusted
|
||||
const seedDiv = document.createElement("div");
|
||||
seedDiv.style.cssText = "color: #fff; font-weight: bold; margin-bottom: 1px;";
|
||||
seedDiv.textContent = `🎲 ${Number(item.seed)}`;
|
||||
const timeDiv = document.createElement("div");
|
||||
timeDiv.style.cssText = "color: #999; font-size: 8px;";
|
||||
timeDiv.textContent = `⏰ ${timeAgo}`;
|
||||
entryDiv.appendChild(seedDiv);
|
||||
entryDiv.appendChild(timeDiv);
|
||||
|
||||
this.historyDisplay.appendChild(entryDiv);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user