diff --git a/__init__.py b/__init__.py index 194fea7..257ac9b 100644 --- a/__init__.py +++ b/__init__.py @@ -8,4 +8,4 @@ from .nf_preview_selector import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS WEB_DIRECTORY = "./js" __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] -pass # NF Preview Selector loaded +pass diff --git a/js/floating_window.css b/js/floating_window.css index 59df4f7..3d093ed 100644 --- a/js/floating_window.css +++ b/js/floating_window.css @@ -72,12 +72,32 @@ align-items: start; } +/* ComfyUI-style layout with optimized calculations */ +.nf-image-grid.comfyui-layout { + display: grid; + grid-template-columns: repeat(var(--max-columns, auto-fit), var(--min-column-width, 150px)); + grid-template-rows: repeat(var(--max-rows, auto-fit), var(--cell-height, auto)); + gap: 8px; + align-items: center; + justify-content: center; + align-content: start; +} + .nf-image-container { position: relative; border: 2px solid transparent; border-radius: 4px; cursor: pointer; transition: border-color 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +/* ComfyUI-style image containers with fixed dimensions */ +.nf-image-grid.comfyui-layout .nf-image-container { + width: var(--min-column-width, 150px); + height: var(--cell-height, 150px); } /* Responsive image containers */ @@ -102,6 +122,13 @@ border-radius: 2px; } +/* ComfyUI-style images with fixed dimensions */ +.nf-image-grid.comfyui-layout .nf-image { + width: 100%; + height: 100%; + object-fit: contain; +} + .nf-image-overlay { position: absolute; top: 4px; diff --git a/js/floating_window.js b/js/floating_window.js index b93a390..b7af0b0 100644 --- a/js/floating_window.js +++ b/js/floating_window.js @@ -10,7 +10,6 @@ export class NFFloatingWindow extends HTMLDivElement { this.moveCallback = moveCallback; this.resizeCallback = resizeCallback; - // Setup window structure this.classList.add('nf-float'); // Header @@ -24,7 +23,6 @@ export class NFFloatingWindow extends HTMLDivElement { this.body.classList.add('nf-float-body'); this.appendChild(this.body); - // Setup dragging this.dragging = false; this.position = { x: x, y: y }; @@ -33,17 +31,14 @@ export class NFFloatingWindow extends HTMLDivElement { document.addEventListener('mousemove', this.drag.bind(this)); document.addEventListener('mouseleave', this.stopDrag.bind(this)); - // Position window this.moveTo(x, y); - // Add to parent or body if (parent) { parent.appendChild(this); } else { document.body.appendChild(this); } - // Setup resize observer for size changes this.setupResizeObserver(); } @@ -110,5 +105,4 @@ export class NFFloatingWindow extends HTMLDivElement { } } -// Register custom element customElements.define('nf-floating-window', NFFloatingWindow, { extends: 'div' }); \ No newline at end of file diff --git a/js/nf_preview_selector.js b/js/nf_preview_selector.js index d7c6470..a11a8f7 100644 --- a/js/nf_preview_selector.js +++ b/js/nf_preview_selector.js @@ -8,28 +8,20 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { nfPreviewDialog } from './preview_dialog.js'; -// NF Preview Selector loading app.registerExtension({ name: "nf.preview_selector", async setup() { - // Setting up extension - // Register message handler using ComfyUI's system if (api.addEventListener) { - // Register for our custom event type api.addEventListener("nf_preview_request", (event) => { - // Received review request nfPreviewDialog.show(event.detail); }); - // Event listener registered } - // Event system is working correctly - // Add settings app.ui.settings.addSetting({ id: "NFPreviewSelector.UI.WindowPosition", name: "Remember window position and size", @@ -46,41 +38,31 @@ app.registerExtension({ tooltip: "Maximum width of the preview dialog in pixels (400-1800)" }); - // Auto-select first image setting removed - this is available in node UI - // Setup complete }, async nodeCreated(node) { if (node.comfyClass === "NFPreviewSelector") { - // Node created - // Set node title node.title = "NF Preview Selector"; - // Add any node-specific setup here node.onResize = function() { - // Handle node resize if needed }; } }, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "NFPreviewSelector") { - // Registering node definition - // Add any node definition modifications here const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) { onNodeCreated.apply(this, arguments); } - // Node-specific initialization this.setProperty("review_position", { x: 100, y: 100 }); }; } } }); -// Extension loaded successfully \ No newline at end of file diff --git a/js/preview_dialog.js b/js/preview_dialog.js index 3866372..325f719 100644 --- a/js/preview_dialog.js +++ b/js/preview_dialog.js @@ -22,7 +22,6 @@ export class NFPreviewDialog { // Load CSS this.loadCSS(); - // Apply max width setting this.applyMaxWidthSetting(); // Setup Queue Prompt keybinding integration @@ -37,14 +36,11 @@ export class NFPreviewDialog { } applyMaxWidthSetting() { - // Get max width setting from ComfyUI settings (try new ID first, fallback to old) let maxWidth = app.ui.settings.getSettingValue("NFPreviewSelector.UI.MaxWidth.v2", app.ui.settings.getSettingValue("NFPreviewSelector.UI.MaxWidth", 800)); - // Validate and clamp the value between 400 and 1800 maxWidth = Math.max(400, Math.min(1800, maxWidth)); - // Create or update CSS rule for max width const style = document.createElement('style'); style.id = 'nf-preview-max-width-style'; style.textContent = ` @@ -53,7 +49,6 @@ export class NFPreviewDialog { } `; - // Remove existing style if it exists const existingStyle = document.getElementById('nf-preview-max-width-style'); if (existingStyle) { existingStyle.remove(); @@ -65,29 +60,19 @@ export class NFPreviewDialog { setupQueuePromptIntegration() { // Listen for Queue Prompt keybinding events document.addEventListener('keydown', (event) => { - // Check if it's Ctrl+Enter (Queue Prompt) or Ctrl+Shift+Enter (Queue Prompt to Front) if (event.ctrlKey && event.key === 'Enter') { - // Only handle if preview dialog is visible if (this.isVisible()) { - // Queue Prompt keybinding detected - - // Stop the dialog processing immediately this.stopDialogForQueue(); - // Allow the event to propagate to ComfyUI's queue system - // Do not prevent default - let ComfyUI handle the queue } } }); - // Also listen for programmatic queue events via API api.addEventListener("status", (event) => { const status = event.detail; - // If queue is starting and dialog is visible, stop the dialog if (status && status.exec_info && status.exec_info.queue_remaining > 0) { if (this.isVisible()) { - // Queue started programmatically this.stopDialogForQueue(); } } @@ -95,15 +80,10 @@ export class NFPreviewDialog { } stopDialogForQueue() { - // Stop countdown immediately this.stopCountdown(); - // Stopping dialog processing for queue - - // Send cancellation response to backend (empty selection, cancelled=true) this.sendSelection([], true); - // If dialog is pinned, don't hide it - just reset for next use if (this.isPinned) { this.prepareForNextRequest(); } else { @@ -116,24 +96,18 @@ export class NFPreviewDialog { } show(data) { - // Showing review dialog - // Apply latest max width setting this.applyMaxWidthSetting(); - // If dialog is already open for this review, don't create another if (this.reviewId === data.review_id && this.window) { - // Dialog already open return; } - // If dialog is pinned and visible, update with new data if (this.isPinned && this.isVisible()) { this.updateWithNewData(data); return; } - // Stop any existing countdown this.stopCountdown(); this.images = data.images || []; @@ -153,19 +127,16 @@ export class NFPreviewDialog { this.window.destroy(); } - // Get saved position or calculate center position const rememberPosition = app.ui.settings.getSettingValue("NFPreviewSelector.UI.WindowPosition", true); const savedPosition = rememberPosition ? this.getSavedPosition() : null; let x, y; if (savedPosition) { - // Use saved position from previous session x = savedPosition.x; y = savedPosition.y; } else { - // First time or position memory disabled: calculate center position - x = Math.max(0, (window.innerWidth - 600) / 2); // 600px is approximate dialog width - y = Math.max(0, (window.innerHeight - 400) / 2); // 400px is approximate dialog height + x = Math.max(0, (window.innerWidth - 600) / 2); + y = Math.max(0, (window.innerHeight - 400) / 2); } this.window = new NFFloatingWindow( @@ -180,7 +151,6 @@ export class NFPreviewDialog { this.setupWindowContent(); this.updateWindowTitle(); - // Apply saved size if available and position memory is enabled if (rememberPosition) { const savedSize = this.getSavedSize(); if (savedSize) { @@ -189,7 +159,6 @@ export class NFPreviewDialog { } } - // Center the dialog only if no saved position (first time) if (!savedPosition) { this.centerDialog(); } @@ -205,12 +174,10 @@ export class NFPreviewDialog { this.controls = document.createElement('div'); this.controls.className = 'nf-controls'; - // Selection info this.selectionInfo = document.createElement('div'); this.selectionInfo.className = 'nf-selection-info'; this.updateSelectionInfo(); - // Countdown this.countdown = document.createElement('div'); this.countdown.className = 'nf-countdown'; @@ -229,7 +196,6 @@ export class NFPreviewDialog { this.cancelButton.textContent = 'Cancel'; this.cancelButton.onclick = this.cancelSelection.bind(this); - // Pin button this.pinButton = document.createElement('button'); this.pinButton.className = 'nf-button pin'; this.pinButton.innerHTML = '📌'; @@ -251,7 +217,6 @@ export class NFPreviewDialog { renderImages() { this.imageGrid.innerHTML = ''; - // Counter for loaded images to determine layout let loadedImages = 0; let landscapeCount = 0; let portraitCount = 0; @@ -267,18 +232,16 @@ export class NFPreviewDialog { img.src = this.getImageUrl(imageInfo); img.alt = `Image ${index + 1}`; - // Check image aspect ratio when loaded img.onload = () => { loadedImages++; const aspectRatio = img.naturalWidth / img.naturalHeight; - if (aspectRatio > 1.1) { // Landscape (wider than tall) + if (aspectRatio > 1.1) { landscapeCount++; - } else if (aspectRatio < 0.9) { // Portrait (taller than wide) + } else if (aspectRatio < 0.9) { portraitCount++; } - // When all images are loaded, determine layout if (loadedImages === this.images.length) { this.applyImageLayout(landscapeCount, portraitCount); } @@ -295,24 +258,94 @@ export class NFPreviewDialog { } applyImageLayout(landscapeCount, portraitCount) { - // Always use smart grid layout that adapts to window size - this.imageGrid.className = 'nf-image-grid smart-grid-layout'; + this.imageGrid.className = 'nf-image-grid comfyui-layout'; - // Set CSS custom properties for dynamic grid sizing - const dialogWidth = this.window.offsetWidth - 48; // Account for padding + const dialogWidth = this.window.offsetWidth - 48; + const dialogHeight = this.window.offsetHeight - 140; + + const layoutResult = this.calculateComfyUILayout(this.images, dialogWidth, dialogHeight); + + this.imageGrid.style.setProperty('--min-column-width', `${layoutResult.cellWidth}px`); + this.imageGrid.style.setProperty('--cell-height', `${layoutResult.cellHeight}px`); + this.imageGrid.style.setProperty('--max-columns', layoutResult.cols); + this.imageGrid.style.setProperty('--max-rows', layoutResult.rows); + + } + + calculateComfyUILayout(images, availableWidth, availableHeight) { + if (!images || images.length === 0) { + return { cellWidth: 200, cellHeight: 200, cols: 1, rows: 1 }; + } + + let imageWidth = 512; + let imageHeight = 512; + + const firstImageContainer = this.imageGrid.querySelector('.nf-image-container img'); + if (firstImageContainer && firstImageContainer.naturalWidth > 0) { + imageWidth = firstImageContainer.naturalWidth; + imageHeight = firstImageContainer.naturalHeight; + } + + const numImages = images.length; + let bestArea = 0; + let bestResult = { cellWidth: 200, cellHeight: 200, cols: 1, rows: 1 }; + + for (let cols = 1; cols <= numImages; cols++) { + const rows = Math.ceil(numImages / cols); + const cellWidth_available = availableWidth / cols; + const cellHeight_available = availableHeight / rows; + + const scaleX = cellWidth_available / imageWidth; + const scaleY = cellHeight_available / imageHeight; + const scale = Math.min(scaleX, scaleY, 1); + + const cellWidth = imageWidth * scale; + const cellHeight = imageHeight * scale; + + const totalDisplayArea = cellWidth * cellHeight * numImages; + + if (totalDisplayArea > bestArea) { + bestArea = totalDisplayArea; + bestResult = { + cellWidth: Math.round(cellWidth), + cellHeight: Math.round(cellHeight), + cols, + rows + }; + } + } + + return bestResult; + } + + calculateResponsiveColumnWidth(landscapeCount, portraitCount) { + const dialogWidth = this.window.offsetWidth - 48; const isLandscapeDominant = landscapeCount > portraitCount; - if (isLandscapeDominant) { - // For landscape images, use slightly wider columns but still allow multiple columns - const minColumnWidth = Math.max(250, Math.min(350, dialogWidth / 2)); // 200-300px, max 2 columns typically - this.imageGrid.style.setProperty('--min-column-width', `${minColumnWidth}px`); - // Using landscape image layout - } else { - // For portrait/square images, use standard grid sizing - const minColumnWidth = Math.max(120, Math.min(250, dialogWidth / 3)); // 200-250px, allow up to 3-4 columns - this.imageGrid.style.setProperty('--min-column-width', `${minColumnWidth}px`); - // Using portrait/square image layout - } + const devicePixelRatio = window.devicePixelRatio || 1; + const dpiMultiplier = Math.min(Math.max(devicePixelRatio, 1), 2); + + const baseRatio = isLandscapeDominant ? 0.45 : 0.30; // 45% for landscape, 30% for portrait + const maxColumns = isLandscapeDominant ? 2 : 4; + + const baseColumnWidth = dialogWidth * baseRatio; + + const dpiAwareColumnWidth = baseColumnWidth * dpiMultiplier; + + const minWidthForColumns = dialogWidth / maxColumns; + + const absoluteMin = isLandscapeDominant ? 200 : 120; + const absoluteMax = isLandscapeDominant ? 600 : 400; + + const finalColumnWidth = Math.max( + absoluteMin, + Math.min( + absoluteMax, + Math.max(minWidthForColumns, dpiAwareColumnWidth) + ) + ); + + return Math.round(finalColumnWidth); } getImageUrl(imageInfo) { @@ -333,7 +366,6 @@ export class NFPreviewDialog { this.updateSelectionInfo(); this.updateConfirmButton(); - // Selected indices updated } updateSelectionInfo() { @@ -357,7 +389,6 @@ export class NFPreviewDialog { this.updateCountdown(); if (this.remainingTime <= 0) { - // Timeout reached this.timeoutSelection(); } }, 1000); @@ -380,11 +411,9 @@ export class NFPreviewDialog { this.stopCountdown(); const selection = Array.from(this.selectedIndices).sort((a, b) => a - b); - // Confirming selection this.sendSelection(selection, false); - // If pinned, don't hide dialog - just reset for next use if (this.isPinned) { this.prepareForNextRequest(); } else { @@ -395,10 +424,8 @@ export class NFPreviewDialog { cancelSelection() { this.stopCountdown(); - // Cancelling selection this.sendSelection([], true); - // If pinned, don't hide dialog - just reset for next use if (this.isPinned) { this.prepareForNextRequest(); } else { @@ -407,19 +434,16 @@ export class NFPreviewDialog { } prepareForNextRequest() { - // Reset dialog state for next request while keeping it open this.selectedIndices.clear(); this.clearImageSelection(); this.updateSelectionInfo(); this.updateConfirmButton(); this.stopCountdown(); - // Clear images and show waiting message this.images = []; this.reviewId = null; this.uniqueId = null; - // Show waiting message this.showWaitingMessage(); } @@ -435,13 +459,10 @@ export class NFPreviewDialog { timeoutSelection() { this.stopCountdown(); - // On timeout, select all images or none based on setting - const selection = []; // Default to empty selection on timeout - // Timeout - returning empty selection + const selection = []; this.sendSelection(selection, false); - // If pinned, don't hide dialog - just reset for next use if (this.isPinned) { this.prepareForNextRequest(); } else { @@ -457,7 +478,6 @@ export class NFPreviewDialog { cancelled: cancelled }; - // Send response to backend fetch('/nf_preview_response', { method: 'POST', headers: { @@ -465,7 +485,6 @@ export class NFPreviewDialog { }, body: JSON.stringify(response) }).catch(error => { - console.error("NF Preview Dialog: Error sending response:", error); }); } @@ -483,7 +502,6 @@ export class NFPreviewDialog { centerDialog() { if (!this.window) return; - // Wait for the DOM to update and get actual dimensions setTimeout(() => { const dialogRect = this.window.getBoundingClientRect(); const centerX = Math.max(0, (window.innerWidth - dialogRect.width) / 2); @@ -498,14 +516,12 @@ export class NFPreviewDialog { const saved = localStorage.getItem('nf-preview-dialog-position'); if (saved) { const position = JSON.parse(saved); - // Validate position is within screen bounds if (position.x >= 0 && position.y >= 0 && position.x < window.innerWidth && position.y < window.innerHeight) { return position; } } } catch (e) { - console.warn("NF Preview Dialog: Error loading saved position:", e); } return null; } @@ -514,7 +530,6 @@ export class NFPreviewDialog { try { localStorage.setItem('nf-preview-dialog-position', JSON.stringify({ x, y })); } catch (e) { - console.warn("NF Preview Dialog: Error saving position:", e); } } @@ -523,14 +538,12 @@ export class NFPreviewDialog { const saved = localStorage.getItem('nf-preview-dialog-size'); if (saved) { const size = JSON.parse(saved); - // Validate size is reasonable if (size.width > 200 && size.height > 150 && size.width < window.innerWidth + 100 && size.height < window.innerHeight + 100) { return size; } } } catch (e) { - console.warn("NF Preview Dialog: Error loading saved size:", e); } return null; } @@ -539,29 +552,23 @@ export class NFPreviewDialog { try { localStorage.setItem('nf-preview-dialog-size', JSON.stringify({ width, height })); } catch (e) { - console.warn("NF Preview Dialog: Error saving size:", e); } } onWindowMove(x, y) { - // Save position when user moves the dialog (only if setting is enabled) const rememberPosition = app.ui.settings.getSettingValue("NFPreviewSelector.UI.WindowPosition", true); if (rememberPosition) { this.savePosition(x, y); } - // Window moved } onWindowResize(width, height) { - // Save size when user resizes the dialog (only if setting is enabled) const rememberPosition = app.ui.settings.getSettingValue("NFPreviewSelector.UI.WindowPosition", true); if (rememberPosition) { this.saveSize(width, height); } - // Recalculate grid layout for new window size if (this.imageGrid && this.imageGrid.children.length > 0) { - // Get current aspect ratio distribution let landscapeCount = 0; let portraitCount = 0; @@ -580,7 +587,6 @@ export class NFPreviewDialog { this.applyImageLayout(landscapeCount, portraitCount); } - // Window resized } togglePin() { @@ -606,10 +612,8 @@ export class NFPreviewDialog { } updateWithNewData(data) { - // Stop current countdown this.stopCountdown(); - // Update data this.reviewId = data.review_id; this.uniqueId = data.unique_id; this.images = data.images || []; @@ -617,7 +621,6 @@ export class NFPreviewDialog { this.remainingTime = this.timeout; this.selectedIndices.clear(); - // Re-render images and restart countdown this.renderImages(); this.updateSelectionInfo(); this.updateConfirmButton(); diff --git a/nf_preview_selector.py b/nf_preview_selector.py index eb21d16..92b35f1 100644 --- a/nf_preview_selector.py +++ b/nf_preview_selector.py @@ -26,7 +26,7 @@ async def handle_review_response(request): selection = data.get('selection', []) cancelled = data.get('cancelled', False) - pass # Response received + pass # Store response review_responses[review_id] = { @@ -36,7 +36,7 @@ async def handle_review_response(request): return web.json_response({'status': 'success'}) except Exception as e: - pass # Error handled + pass return web.json_response({'status': 'error', 'message': str(e)}, status=500) # Register API endpoint @@ -78,7 +78,7 @@ class NFPreviewSelector(PreviewImage): def preview_images(self, images, mode, timeout, unique_id=None, latents=None, selection_indices="", prompt=None, extra_pnginfo=None): """Main function for image preview and selection""" - pass # Processing images + pass # No flags needed anymore @@ -115,9 +115,9 @@ class NFPreviewSelector(PreviewImage): # Use ComfyUI's message dispatch system try: PromptServer.instance.send_sync("nf_preview_request", message_data) - pass # Request sent + pass except Exception as e: - pass # Fallback to direct WebSocket + pass # Fallback to direct WebSocket message = { "type": "nf_preview_request", @@ -129,20 +129,20 @@ class NFPreviewSelector(PreviewImage): try: await ws.send_str(json.dumps(message)) except Exception as e: - pass # Send failed + pass if PromptServer.instance.loop: asyncio.run_coroutine_threadsafe(send_message(), PromptServer.instance.loop) - pass # Request sent + pass # Wait for response from frontend result = self.wait_for_response(review_id, timeout) if result == "CANCELLED": - pass # Cancelled + pass raise InterruptProcessingException() elif result == "TIMEOUT": - pass # Timed out + pass raise InterruptProcessingException() else: selected_indices = result @@ -154,7 +154,7 @@ class NFPreviewSelector(PreviewImage): # Process selection if not selected_indices: # For timeout or other cases, return empty tensors - pass # No selection + pass empty_image = torch.zeros((1, images.shape[1], images.shape[2], images.shape[3])) empty_latent = {"samples": torch.zeros((1, 4, 64, 64))} if latents is None else {"samples": torch.zeros((1, latents["samples"].shape[1], latents["samples"].shape[2], latents["samples"].shape[3]))} return (empty_image, empty_latent, "") @@ -172,7 +172,7 @@ class NFPreviewSelector(PreviewImage): indices_str = ",".join(str(i) for i in selected_indices) - pass # Selection made + pass return { "result": (selected_images, selected_latents, indices_str), @@ -190,16 +190,16 @@ class NFPreviewSelector(PreviewImage): response = review_responses.pop(review_id) if response['cancelled']: - pass # Review cancelled + pass return "CANCELLED" else: selection = response['selection'] - pass # Review completed + pass return selection time.sleep(0.1) # Poll every 100ms - pass # Review timed out + pass return "TIMEOUT" # Indicate timeout occurred # Node registration diff --git a/pyproject.toml b/pyproject.toml index dfdc64a..ed7fd1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "Preview_Selector" description = "A floating dialog-based image preview and selection system for ComfyUI." -version = "1.1.1" +version = "1.2.0" license = {file = "LICENSE"}