From e82618d2bad6c7760220bf674f50573293be9631 Mon Sep 17 00:00:00 2001 From: AEmotionStudio Date: Wed, 31 Dec 2025 14:09:54 -0800 Subject: [PATCH] feat: Reimplement magnify info panel with state management and theme detection, and add browser console errors documentation. --- web/magnify_info_panel.js | 3073 +------------------------- web/magnify_info_panel.js.backup | 3035 +++++++++++++++++++++++++ web/magnify_info_panel_refactored.js | 60 - web/shared/settings.js | 143 ++ 4 files changed, 3227 insertions(+), 3084 deletions(-) create mode 100644 web/magnify_info_panel.js.backup delete mode 100644 web/magnify_info_panel_refactored.js diff --git a/web/magnify_info_panel.js b/web/magnify_info_panel.js index 58cc47f..dea1ef4 100644 --- a/web/magnify_info_panel.js +++ b/web/magnify_info_panel.js @@ -1,3035 +1,60 @@ -import { app } from "../../../scripts/app.js"; - /** - * ComfyUI Magnifying Glass - Professional Information Panel Extension (Refactored) + * ComfyUI MagnifyGlass - Info Panel (Refactored Entry Point) * - * This script provides a professional, highly interactive information panel that displays - * detailed analysis of whatever you're hovering over when the magnifying glass is active. - * - * Key Features: - * - Unified state management - * - Smooth mode transitions - * - Improved pin functionality with drag support - * - Lock feature to prevent dragging when pinned - * - Clean control system - * - Better positioning logic + * This is the refactored version of the info panel that uses modular imports. + * The original magnify_info_panel.js is preserved as a backup. */ +import { app } from "../../../scripts/app.js"; +import { InfoPanel } from './info-panel/index.js'; +import { DEFAULT_PANEL_SETTINGS, getSettingConfig } from './shared/settings.js'; + app.registerExtension({ - name: "comfyui.magnify.glass.info.panel.pro.v2", + name: "Comfy.MagnifyGlass.InfoPanel.Refactored", + async setup() { - // Wait for the main magnifying glass to be ready - let magnifyGlass = null; - let retryCount = 0; - const maxRetries = 50; - - const waitForMagnifyGlass = () => { - if (window.comfyUIMagnifyGlass) { - magnifyGlass = window.comfyUIMagnifyGlass; - console.log("ComfyUI Magnify Info Panel Pro V2: Found magnifying glass instance"); - initializeInfoPanel(); - } else if (retryCount < maxRetries) { - retryCount++; - setTimeout(waitForMagnifyGlass, 100); - } else { - console.warn("ComfyUI Magnify Info Panel Pro V2: Could not find magnifying glass instance after 5 seconds"); - } - }; - - setTimeout(waitForMagnifyGlass, 100); - - function initializeInfoPanel() { - // Default settings configuration - const DEFAULT_SETTINGS = { - "🔍MagnifyGlass.InfoPanelEnabled": true, - "🔍MagnifyGlass.InfoPanelPosition": "Bottom", - "🔍MagnifyGlass.InfoPanelWidth": 320, - "🔍MagnifyGlass.InfoPanelOpacity": 100, - "🔍MagnifyGlass.InfoPanelMaxHeight": 1000, - "🔍MagnifyGlass.InfoPanelAnimations": false, - "🔍MagnifyGlass.ShowInspectorTab": false, - "🔍MagnifyGlass.ToggleHotkey": "i", - "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", - "🔍MagnifyGlass.PinPanelHotkey": "u", - "🔍MagnifyGlass.ShowHoveringControls": true, - "🔍MagnifyGlass.ControlsPosition": "bottom", - "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", - "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", - }; - - const getSettingValue = (key, defaultValue) => { - try { - const value = app.ui.settings.getSettingValue(key); - return value === undefined ? defaultValue : value; - } catch (e) { - console.warn(`ComfyUI Magnify Info Panel Pro V2: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); - return defaultValue; - } - }; - - /** - * Centralized State Manager - * Handles all state-related operations and provides clean state transitions - */ - class StateManager { - constructor() { - this.state = { - // Panel visibility and positioning - isPanelVisible: false, - isPanelMinimized: false, - isPanelPinned: false, - isPanelLocked: false, // New lock state - isAutoPinned: false, // Track if panel was auto-pinned by glass hide - pinnedPosition: { x: 0, y: 0 }, - lastPinnedPosition: null, // Remember last pinned location - - // Glass visibility - isGlassPreviewVisible: true, - - // Section expansion - expandedSections: new Set(['node']), - - // Interaction states - isPanelHovered: false, - isHoveringNode: false, - - // Auto-collapse - autoExpandTimer: null, - lastNodeId: null, - - // Update scheduling - updateScheduled: false, - isInitialLoading: false, - - // Current data - currentInfo: {}, - - // Settings cache - settings: {}, - - // Auto-detected theme - currentTheme: 'dark' // Will be auto-detected - }; - - this.initThemeDetection(); - this.loadSettings(); - } - - initThemeDetection() { - // Immediate forced detection - // console.log('ComfyUI Magnify Glass: Starting theme detection...'); - - // Detect ComfyUI's current theme - this.detectCurrentTheme(); - - // Force an immediate check in case we missed the initial state - setTimeout(() => { - // console.log('ComfyUI Magnify Glass: Delayed theme check...'); - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 1000); - - // Set up mutation observer to watch for theme changes - this.setupThemeObserver(); - - // Listen for system theme changes as fallback - if (window.matchMedia) { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); - prefersDark.addListener(() => this.detectCurrentTheme()); - } - - // Add click listener to theme toggle buttons for immediate detection - this.setupThemeButtonListeners(); - } - - detectCurrentTheme() { - let detectedTheme = 'dark'; // Default fallback - - const body = document.body; - const html = document.documentElement; - const vueApp = document.querySelector('#vue-app'); - const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); - - // Get all possible theme-related elements - const allElements = [body, html, vueApp, sidebar].filter(Boolean); - - // Debug: Log current classes and attributes with more detail - const debugInfo = { - bodyClasses: Array.from(body.classList), - htmlClasses: Array.from(html.classList), - bodyDataTheme: body.getAttribute('data-theme'), - htmlDataTheme: html.getAttribute('data-theme'), - vueAppClasses: vueApp ? Array.from(vueApp.classList) : null, - sidebarClasses: sidebar ? Array.from(sidebar.classList) : null, - bodyBgColor: window.getComputedStyle(body).backgroundColor, - htmlBgColor: window.getComputedStyle(html).backgroundColor, - allComputedVars: {} - }; - - // Check CSS variables - const rootStyles = window.getComputedStyle(html); - const bodyStyles = window.getComputedStyle(body); - - // Get all CSS custom properties - for (let i = 0; i < rootStyles.length; i++) { - const prop = rootStyles[i]; - if (prop.startsWith('--')) { - debugInfo.allComputedVars[prop] = rootStyles.getPropertyValue(prop).trim(); - } - } - - // console.log('ComfyUI Magnify Glass: Comprehensive theme detection debug:', debugInfo); - - // Method 1: Look for specific light theme indicators in CSS variables - const primarySurface = rootStyles.getPropertyValue('--p-primary-color') || - rootStyles.getPropertyValue('--primary-color') || - bodyStyles.getPropertyValue('--p-primary-color') || - bodyStyles.getPropertyValue('--primary-color'); - - const surfaceGround = rootStyles.getPropertyValue('--p-surface-ground') || - rootStyles.getPropertyValue('--surface-ground') || - bodyStyles.getPropertyValue('--p-surface-ground') || - bodyStyles.getPropertyValue('--surface-ground'); - - const textColor = rootStyles.getPropertyValue('--p-text-color') || - rootStyles.getPropertyValue('--text-color') || - bodyStyles.getPropertyValue('--p-text-color') || - bodyStyles.getPropertyValue('--text-color'); - - // console.log('ComfyUI Magnify Glass: CSS Variables:', { primarySurface, surfaceGround, textColor }); - - // Method 2: Analyze background colors of multiple elements - const backgrounds = allElements.map(el => { - const styles = window.getComputedStyle(el); - return { - element: el.tagName || el.className, - backgroundColor: styles.backgroundColor, - color: styles.color - }; - }); - - // console.log('ComfyUI Magnify Glass: Background analysis:', backgrounds); - - // Method 3: Check for light backgrounds - for (const bg of backgrounds) { - if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { - const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - // console.log(`ComfyUI Magnify Glass: Element ${bg.element} brightness: ${brightness}`); - if (brightness > 180) { // Higher threshold for light detection - detectedTheme = 'light'; - // console.log(`ComfyUI Magnify Glass: Light theme detected via background analysis of ${bg.element}`); - break; - } - } - } - } - - // Method 4: Check for theme toggle buttons state (they might indicate current theme) - const themeButtons = document.querySelectorAll('[class*="theme"], button[title*="theme"], button[title*="Theme"], .p-button[title*="light"], .p-button[title*="Light"]'); - themeButtons.forEach(btn => { - // console.log('ComfyUI Magnify Glass: Found theme button:', btn); - }); - - // Method 5: Force light theme detection if we see light backgrounds - if (detectedTheme === 'dark') { - // Look for any element with very light background - const lightElements = document.querySelectorAll('*'); - for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements - const el = lightElements[i]; - const styles = window.getComputedStyle(el); - const bgColor = styles.backgroundColor; - - if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { - const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - if (brightness > 200) { // Very light elements - detectedTheme = 'light'; - // console.log(`ComfyUI Magnify Glass: Light theme forced via element ${el.tagName}.${el.className} with brightness ${brightness}`); - break; - } - } - } - } - } - - // Update theme if it changed - if (this.state.currentTheme !== detectedTheme) { - // console.log(`ComfyUI Magnify Glass: Theme changed from ${this.state.currentTheme} to ${detectedTheme}`); - this.state.currentTheme = detectedTheme; - return true; // Theme changed - } - - // console.log(`ComfyUI Magnify Glass: Current theme remains: ${detectedTheme}`); - return false; // No change - } - - setupThemeObserver() { - // Watch for changes to body/html classes and attributes - const observer = new MutationObserver((mutations) => { - let shouldCheck = false; - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - (mutation.attributeName === 'class' || - mutation.attributeName === 'data-theme' || - mutation.attributeName === 'style')) { - shouldCheck = true; - // console.log('ComfyUI Magnify Glass: DOM change detected:', { - // target: mutation.target.tagName, - // attribute: mutation.attributeName, - // oldValue: mutation.oldValue, - // newValue: mutation.target.getAttribute(mutation.attributeName) - // }); - } - }); - - if (shouldCheck && this.detectCurrentTheme()) { - // Theme changed, update UI immediately - this.notifyThemeChange(); - } - }); - - // Observe changes to body, html, and vue app - observer.observe(document.body, { attributes: true, attributeOldValue: true }); - observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); - - const vueApp = document.querySelector('#vue-app'); - if (vueApp) { - observer.observe(vueApp, { attributes: true, attributeOldValue: true }); - } - - // Also watch for changes to any element with theme-related classes - const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); - themeElements.forEach(el => { - observer.observe(el, { attributes: true, attributeOldValue: true }); - }); - - // Periodic fallback check every 2 seconds - setInterval(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 2000); - } - - setupThemeButtonListeners() { - // Listen for clicks on theme toggle buttons - document.addEventListener('click', (e) => { - // Check if clicked element might be a theme toggle - const target = e.target; - const isThemeButton = target.textContent?.toLowerCase().includes('theme') || - target.textContent?.toLowerCase().includes('light') || - target.textContent?.toLowerCase().includes('dark') || - target.title?.toLowerCase().includes('theme') || - target.className?.toLowerCase().includes('theme'); - - if (isThemeButton) { - // console.log('ComfyUI Magnify Glass: Theme button clicked, triggering detection'); - setTimeout(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 100); - } - }); - } - - notifyThemeChange() { - // Update settings to reflect new theme - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - - // Notify UI components of theme change immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); - } - } - - loadSettings() { - Object.keys(DEFAULT_SETTINGS).forEach(key => { - // Skip the theme setting since we auto-detect it - if (key !== "🔍MagnifyGlass.InfoPanelTheme") { - this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); - } - }); - - // Set theme to auto-detected value - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - } - - updateSettings() { - const oldSettings = { ...this.state.settings }; - this.loadSettings(); - - // Return what changed for reactive updates - const changes = {}; - Object.keys(this.state.settings).forEach(key => { - if (oldSettings[key] !== this.state.settings[key]) { - changes[key] = { - old: oldSettings[key], - new: this.state.settings[key] - }; - } - }); - - return changes; - } - - togglePanelVisibility() { - this.state.isPanelVisible = !this.state.isPanelVisible; - // console.log(`Info panel visibility toggled to ${this.state.isPanelVisible ? 'visible' : 'hidden'}`); - return this.state.isPanelVisible; - } - - toggleGlassPreview() { - this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; - // console.log(`Glass preview visibility toggled to ${this.state.isGlassPreviewVisible ? 'visible' : 'hidden'}`); - return this.state.isGlassPreviewVisible; - } - - togglePinning() { - const wasPinned = this.state.isPanelPinned; - this.state.isPanelPinned = !this.state.isPanelPinned; - - // When unpinning, also unlock and clear auto-pin flag - if (!this.state.isPanelPinned) { - this.state.isPanelLocked = false; - this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning - } - - if (this.state.isPanelPinned && this.state.lastPinnedPosition) { - // Restore to last pinned position - this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; - // console.log(`Panel pinned at remembered position: (${this.state.pinnedPosition.x}, ${this.state.pinnedPosition.y})`); - } else if (!this.state.isPanelPinned && wasPinned) { - // Save current position when unpinning - this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; - // console.log(`Panel unpinned, position saved: (${this.state.lastPinnedPosition.x}, ${this.state.lastPinnedPosition.y})`); - } - - // console.log(`Panel ${this.state.isPanelPinned ? 'pinned' : 'unpinned'}`); - return this.state.isPanelPinned; - } - - toggleLocking() { - // Only allow locking when pinned - if (!this.state.isPanelPinned) { - // console.log("Cannot lock panel when not pinned"); - return false; - } - - this.state.isPanelLocked = !this.state.isPanelLocked; - // console.log(`Panel ${this.state.isPanelLocked ? 'locked' : 'unlocked'}`); - return this.state.isPanelLocked; - } - - toggleMinimized() { - this.state.isPanelMinimized = !this.state.isPanelMinimized; - return this.state.isPanelMinimized; - } - - toggleSection(sectionId) { - if (sectionId === 'node') return false; // Node section always expanded - - if (this.state.expandedSections.has(sectionId)) { - this.state.expandedSections.delete(sectionId); + // Wait for magnify glass to be ready + const checkForMagnifyGlass = () => { + return new Promise((resolve) => { + const check = () => { + if (window.comfyUIMagnifyGlass) { + resolve(window.comfyUIMagnifyGlass); } else { - this.state.expandedSections.add(sectionId); - } - return true; - } - - setPinnedPosition(x, y) { - this.state.pinnedPosition = { x, y }; - // Also save as last pinned position for memory - this.state.lastPinnedPosition = { x, y }; - } - - setCurrentInfo(info) { - this.state.currentInfo = info; - } - - scheduleAutoCollapse() { - this.clearAutoExpandTimer(); - if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { - this.state.autoExpandTimer = setTimeout(() => { - if (!this.state.isPanelHovered) { - this.collapseNodeSections(); - } - }, 1500); - } - } - - clearAutoExpandTimer() { - if (this.state.autoExpandTimer) { - clearTimeout(this.state.autoExpandTimer); - this.state.autoExpandTimer = null; - } - } - - expandNodeSections() { - this.state.expandedSections.add('hoveredNode'); - this.state.expandedSections.add('node'); - this.state.expandedSections.add('cursor'); - this.state.expandedSections.add('canvas'); - this.state.expandedSections.add('magnifier'); - this.clearAutoExpandTimer(); - } - - collapseNodeSections() { - this.state.expandedSections.delete('hoveredNode'); - this.state.expandedSections.delete('node'); - this.state.expandedSections.delete('widget'); - } - - cleanup() { - this.clearAutoExpandTimer(); - } - } - - /** - * UI Manager - * Handles all DOM manipulation and UI creation - */ - class UIManager { - constructor(stateManager) { - this.stateManager = stateManager; - this.elements = { - panel: null, - header: null, - content: null, - controls: null - }; - - this.createPanel(); - this.injectStyles(); - } - - createPanel() { - // Main panel container - this.elements.panel = document.createElement("div"); - this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; - this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; - - // Header - this.elements.header = document.createElement("div"); - this.elements.header.className = "panel-header"; - this.elements.header.innerHTML = ` -
-
🔍
-
Inspector
-
Real-time analysis
-
-
- -
- `; - - // Content container - this.elements.content = document.createElement("div"); - this.elements.content.className = "panel-content"; - - this.elements.panel.appendChild(this.elements.header); - this.elements.panel.appendChild(this.elements.content); - - this.applyStyles(); - document.body.appendChild(this.elements.panel); - - // Create floating controls after panel is in DOM - if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { - this.createFloatingControls(); - // Update control states immediately after creation - this.updateControlStates(); - } - } - - createFloatingControls() { - this.elements.controls = document.createElement("div"); - this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical - this.elements.controls.innerHTML = ` - - - - - `; - - // Insert before the panel in the document body, not as a child - document.body.appendChild(this.elements.controls); - - // Set initial layout based on settings - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - this.updateControlsLayout(controlsPosition); - } - - updateControlStates() { - if (!this.elements.controls) return; - - const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); - const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); - const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); - const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); - - if (pinBtn) { - pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); - pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; - pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; - } - - if (lockBtn) { - // Only show/enable lock button when pinned - lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; - lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); - lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; - lockBtn.disabled = !this.stateManager.state.isPanelPinned; - } - - if (visibilityBtn) { - visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); - visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; - } - - if (glassBtn) { - glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); - glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; - } - } - - updateControlsLayout(position) { - if (!this.elements.controls) return; - - // Remove existing layout classes - this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); - - // Add appropriate layout class - if (['top', 'bottom'].includes(position)) { - this.elements.controls.classList.add('horizontal-layout'); - } else { - this.elements.controls.classList.add('vertical-layout'); - } - } - - applyStyles() { - const settings = this.stateManager.state.settings; - - // Apply custom text color via CSS variable if provided - const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; - if (textColor) { - // Ensure color has # symbol - const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; - this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); - } - - // Apply custom accent color via CSS variable if provided - const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (accentColor) { - // Ensure color has # symbol - const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; - this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); - } - - // Apply opacity setting if panel is visible (convert percentage to decimal) - if (this.stateManager.state.isPanelVisible) { - const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - } - - this.elements.panel.style.cssText = ` - position: absolute; - width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; - max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; - z-index: 99999; - display: none; - opacity: 0; - transform: translateY(-10px); - transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; - pointer-events: auto; - user-select: none; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} - ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} - `; - } - - show() { - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.elements.panel.style.display = "block"; - if (this.elements.controls) { - this.elements.controls.style.display = "flex"; - } - // Apply user's opacity setting when showing (convert percentage to decimal) - const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - // Trigger reflow - this.elements.panel.offsetHeight; - this.elements.panel.classList.add('visible'); - this.stateManager.state.isPanelVisible = true; - } - } - - hide() { - this.elements.panel.classList.remove('visible'); - setTimeout(() => { - if (!this.stateManager.state.isPanelVisible) { - this.elements.panel.style.display = "none"; - if (this.elements.controls) { - this.elements.controls.style.display = "none"; - } - } - }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); - this.stateManager.state.isPanelVisible = false; - } - - updateMinimizedState() { - this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); - const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); - if (minimizeBtn) { - minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; - minimizeBtn.title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; - } - } - - updatePinnedState() { - this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); - this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); - this.updateControlStates(); - } - - updateTheme(newTheme) { - if (this.elements.panel) { - this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); - // console.log(`ComfyUI Magnify Glass: Panel theme updated to ${newTheme}`); - } - } - - displayInfo(info) { - const sections = this.buildSections(info); - this.renderSections(sections); - this.updateSectionStates(); - this.updateHeaderSubtitle(info); - } - - buildSections(info) { - const sections = []; - const settings = this.stateManager.state.settings; - - // Inspector section - if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { - const inspectorContent = [ - { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, - { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, - { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } - ]; - - sections.push({ - id: 'inspector', - icon: '🔍', - title: 'Inspector', - content: inspectorContent - }); - } - - // Media section - if (info.media) { - const mediaContent = [ - { label: 'Type', value: info.media.tagName }, - { label: 'Source', value: info.media.src } - ]; - - if (info.media.naturalSize) { - mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); - } - - sections.push({ - id: 'media', - icon: '📷', - title: 'Media', - badge: info.media.tagName, - content: mediaContent - }); - } - - // Node Details section - if (info.hoveredNode) { - const nodeContent = [ - { label: 'Title', value: info.hoveredNode.title } - ]; - - // Check if this is a complex node that shows all widgets - const nodeType = info.hoveredNode.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = info.hoveredNode.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (!showAllWidgets) { - // Add specific extractions for simple nodes - const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); - if (checkpointInfo) { - nodeContent.push({ label: 'Model', value: checkpointInfo }); - } - - const imageInfo = this.getImageInfo(info.hoveredNode); - if (imageInfo) { - if (typeof imageInfo === 'string') { - nodeContent.push({ label: 'Image', value: imageInfo }); - } else if (typeof imageInfo === 'object') { - nodeContent.push({ label: 'Image Size', value: `${imageInfo.width}×${imageInfo.height}` }); - if (imageInfo.src) { - nodeContent.push({ label: 'Image Source', value: imageInfo.src }); - } - } - } - - const textBoxContent = this.getTextBoxContent(info.hoveredNode); - if (textBoxContent) { - nodeContent.push({ label: 'Text', value: textBoxContent }); - } - } - - const importantParameters = this.getImportantNodeParameters(info.hoveredNode); - nodeContent.push(...importantParameters); - - sections.push({ - id: 'node', - icon: '🎯', - title: 'Node', - badge: info.hoveredNode.type, - content: nodeContent - }); - } - - return sections; - } - - renderSections(sections) { - if (sections.length === 0) { - this.elements.content.innerHTML = ` -
-
📍
-
Empty canvas area
-
- `; - return; - } - - this.elements.content.innerHTML = sections.map(section => ` -
-
- ${section.icon} - ${section.title} - ${section.badge ? `${section.badge}` : ''} - ${section.id !== 'node' ? '▶' : ''} -
-
-
- ${section.content.map(item => { - const value = this.formatValue(item.value, item.label); - const valueClass = this.getValueClass(item.value); - const valueAttributes = this.getValueAttributes(item.value); - return ` -
- ${item.label} - ${value} -
`; - }).join('')} -
-
-
- `).join(''); - } - - updateSectionStates() { - const sections = this.elements.content.querySelectorAll('.info-section'); - sections.forEach(section => { - const sectionId = section.dataset.section; - const header = section.querySelector('.section-header'); - const content = section.querySelector('.section-content'); - - if (this.stateManager.state.expandedSections.has(sectionId)) { - header.classList.add('expanded'); - content.classList.add('expanded'); - } else { - header.classList.remove('expanded'); - content.classList.remove('expanded'); - } - }); - } - - formatValue(value, label) { - if (!value) return value; - - const str = String(value); - - // Show full text for prompts, text content, model names, and file paths - if (label && ( - label.toLowerCase().includes('text') || - label.toLowerCase().includes('prompt') || - label.toLowerCase().includes('model') || - label.toLowerCase().includes('file') || - label.toLowerCase().includes('conditioning') || - label.toLowerCase().includes('positive') || - label.toLowerCase().includes('negative') - )) { - return str; - } - - // Show full text for very long values (no truncation) - return str; - } - - getValueClass(value) { - if (!value) return ''; - - const str = String(value); - let classes = []; - - // Mark text that might benefit from special styling for readability - if (str.length > 100) { - classes.push('long-text'); - } - - return classes.join(' '); - } - - getValueAttributes(value) { - // Since we show full text now, we don't need title attributes for long text - // Only add title for very long text that might benefit from tooltips - if (!value) return ''; - - const str = String(value); - if (str.length > 500) { // Only for extremely long text - return `title="${str.replace(/"/g, '"')}"`; - } - - return ''; - } - - updateHeaderSubtitle(info) { - const subtitleElement = this.elements.header.querySelector('.header-subtitle'); - if (subtitleElement) { - const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (info.hoveredNode) { - subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; - subtitleElement.style.color = accentColor; - } else if (info.media) { - subtitleElement.textContent = `Media: ${info.media.tagName}`; - subtitleElement.style.color = accentColor; - } else if (info.connection) { - subtitleElement.textContent = `Connection: ${info.connection.type}`; - subtitleElement.style.color = accentColor; - } else { - subtitleElement.textContent = 'Real-time analysis'; - subtitleElement.style.color = ''; - } - } - } - - // Helper methods for node information extraction - getCheckpointInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("CheckpointLoader") || - nodeInfo.type.includes("LoadCheckpoint") || - nodeInfo.type.includes("ModelLoader") || - nodeInfo.type.includes("UNETLoader") || - nodeInfo.type.includes("VAELoader") || - nodeInfo.type.includes("LoraLoader") - )) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("model") || - widget.name.toLowerCase().includes("checkpoint") || - widget.name.toLowerCase().includes("ckpt") || - widget.name.toLowerCase().includes("lora") || - widget.name.toLowerCase().includes("vae") || - widget.name.toLowerCase().includes("file") - )) { - const value = String(widget.value); - const filename = value.split(/[\/\\]/).pop(); - return filename || value; - } - } - } - } - return null; - } - - getImageInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("SaveImage") || - nodeInfo.type.includes("PreviewImage") || - nodeInfo.type.includes("VisionOutput") || - nodeInfo.type.includes("ImageOutput") || - nodeInfo.type.includes("LoadImage") || - nodeInfo.type.includes("Display") - )) { - if (nodeInfo.widgets) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("image") || - widget.name.toLowerCase().includes("filename") || - widget.name.toLowerCase().includes("file") - )) { - return widget.value; - } - } - } - - if (nodeInfo.properties && nodeInfo.properties.img) { - const img = nodeInfo.properties.img; - return { - width: img.width || "unknown", - height: img.height || "unknown", - src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" - }; - } - - if (nodeInfo.outputs) { - for (const output of nodeInfo.outputs) { - if (output.links && output.links.length > 0) { - return "Image connected to " + output.links.length + " node(s)"; - } - } - } - - return "Image node"; - } - return null; - } - - getTextBoxContent(nodeInfo) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { - for (const widget of nodeInfo.widgets) { - if (widget.name === "text" && typeof widget.value === 'string') { - return widget.value; - } - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.name.toLowerCase().includes("prompt") || - widget.name.toLowerCase().includes("conditioning")) && - typeof widget.value === 'string' && widget.value.length > 0) { - return widget.value; - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.type === 'text' || widget.type === 'textarea' || - widget.type === 'string' || widget.name.toLowerCase().includes('text')) && - typeof widget.value === 'string' && widget.value.length > 0) { - - // Show full text without truncation - return widget.value; - } - } - } - return null; - } - - getImportantNodeParameters(nodeInfo) { - const parameters = []; - - // For complex nodes that have many parameters, show ALL widgets - const nodeType = nodeInfo.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = nodeInfo.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (showAllWidgets) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - // Show all widgets for complex nodes, but filter out some duplicates - if (widget.name && widget.name !== '') { - const widgetName = widget.name.toLowerCase(); - - // Skip some generic widget names that might be duplicates - // These are handled by specific extraction methods for simple nodes - if (widgetName.includes('title') || - widgetName === 'node' || - widgetName === 'id' || - widgetName === 'type' || - widgetName === 'mode') { - continue; - } - - parameters.push({ - label: widget.name, - value: this.formatValue(widget.value) - }); - } - } - } - return parameters; - } - - // Special handling for Save nodes - only show essential parameters - let importantParams; - if (isSaveNode) { - importantParams = [ - 'filename_prefix', 'filename', 'directory', 'path', - 'format', 'quality', 'extension' - ]; - } else { - // For other nodes, use the filtered list but avoid duplicates with specific extractions - importantParams = [ - 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', - 'positive', 'negative', 'width', 'height', 'denoise', 'strength', - 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', - // KSampler specific parameters (for other samplers) - 'control_after_generate', 'control', 'after', 'generate', - 'start_at_step', 'end_at_step', 'start', 'end', - 'return_with_leftover_noise', 'leftover', 'noise_return', - // Additional common parameters (but avoid duplicates with specific extractors) - 'model', 'vae', 'clip', 'lora', 'checkpoint', - 'latent', 'image', 'mask', 'filename', 'directory', - 'prompt', 'conditioning', 'filename_prefix', - // New detection vocabulary - 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', - 'style_type', 'background', 'n', 'human', 'raw', 'guidance', - 'skip_preprocessing', 'movement_amplitude', 'animation', - 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', - 'sigma', 'rho', - // Additional detection vocabulary - 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', - 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', - 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', - 'set_cond_area', 'audioui', - // Camera and 3D parameters - 'camera_pose', 'fx', 'cx', 'fy', 'cy' - // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent - ]; - } - - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - const paramName = widget.name.toLowerCase(); - if (importantParams.some(param => paramName.includes(param))) { - parameters.push({ - label: widget.name, - value: this.formatValue(widget.value) - }); - } - } - } - - return parameters; - } - - formatValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); - } - - injectStyles() { - if (!document.getElementById('magnify-info-panel-styles-v2')) { - const style = document.createElement('style'); - style.id = 'magnify-info-panel-styles-v2'; - style.textContent = this.getCSS(); - document.head.appendChild(style); - } - } - - getCSS() { - return ` - .magnify-info-panel { - border-radius: 12px; - backdrop-filter: blur(20px); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); - overflow: hidden; - font-size: 16px; - line-height: 1.6; - pointer-events: auto; - } - - .magnify-info-panel * { - pointer-events: auto; - } - - .magnify-info-panel.theme-dark { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); - color: var(--info-panel-text-color, #e0e0e0); - border: 1px solid rgba(100, 100, 120, 0.4); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); - backdrop-filter: blur(12px); - } - - .magnify-info-panel.theme-light { - background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); - color: var(--info-panel-text-color, #2a2a2a); - border: 1px solid rgba(200, 200, 220, 0.5); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); - backdrop-filter: blur(12px); - } - - .magnify-info-panel.visible { - transform: translateY(0) !important; - } - - .magnify-info-panel.panel-pinned { - cursor: grab; - box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); - } - - .magnify-info-panel.panel-locked { - cursor: default !important; - box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); - } - - .magnify-info-panel.panel-locked::after { - content: '📌'; - position: absolute; - top: 8px; - right: 8px; - font-size: 14px; - opacity: 0.7; - pointer-events: none; - z-index: 1; - } - - .magnify-info-panel.panel-pinned.panel-dragging { - cursor: grabbing; - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); - transform: scale(1.02) !important; - z-index: 100001 !important; - transition: none !important; - } - - .magnify-info-panel.panel-pinned::before { - content: ''; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); - pointer-events: none; - border-radius: inherit; - } - - .panel-dragging * { - pointer-events: none !important; - user-select: none !important; - transition: none !important; - } - - .magnify-info-panel.panel-minimized .panel-content { - max-height: 0 !important; - overflow: hidden; - transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } - - .floating-controls { - position: fixed; - display: flex; - padding: 4px; - background: rgba(40, 40, 50, 0.95); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - opacity: 1; - z-index: 100000; - backdrop-filter: blur(8px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); - } - - .floating-controls.vertical-layout { - flex-direction: column; - gap: 4px; - } - - .floating-controls.horizontal-layout { - flex-direction: row; - gap: 4px; - } - - .control-btn { - background: rgba(60, 60, 70, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); - width: 28px; - height: 28px; - font-size: 14px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: #ddd; - border-radius: 3px; - transition: background-color 0.15s ease; - } - - .control-btn:hover { - background: rgba(80, 80, 90, 0.9); - color: white; - } - - .control-btn:active { - background: rgba(100, 100, 110, 0.9); - } - - .control-btn:disabled { - background: rgba(40, 40, 50, 0.5); - color: #666; - cursor: not-allowed; - opacity: 0.5; - } - - .control-btn.active { - background: rgba(255, 200, 50, 0.3); - border-color: rgba(255, 200, 50, 0.6); - color: #ffcc33; - } - - .control-btn.lock-btn.active { - background: rgba(255, 69, 0, 0.3); - border-color: rgba(255, 69, 0, 0.6); - color: #ff4500; - } - - .panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - user-select: none; - } - - .header-content { - display: flex; - align-items: center; - gap: 10px; - } - - .header-icon { - font-size: 18px; - opacity: 0.9; - } - - .header-title { - font-weight: 600; - font-size: 18px; - } - - .header-subtitle { - font-size: 14px; - opacity: 0.7; - font-style: italic; - } - - .header-controls { - display: flex; - gap: 6px; - } - - .minimize-btn { - background: rgba(255, 255, 255, 0.1); - border: none; - border-radius: 6px; - color: inherit; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s; - font-weight: bold; - font-size: 16px; - } - - .minimize-btn:hover { - background: rgba(255, 255, 255, 0.2); - transform: scale(1.05); - } - - .minimize-btn:active { - transform: scale(0.95); - } - - .panel-content { - padding: 0; - max-height: calc(100% - 60px); - overflow-y: auto; - scrollbar-width: thin; - } - - .panel-content::-webkit-scrollbar { - width: 8px; - } - - .panel-content::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); - } - - .panel-content::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; - } - - .info-section { - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - margin-bottom: 8px; - border-radius: 10px; - overflow: visible; - background: rgba(255, 255, 255, 0.02); - transition: all 0.3s ease; - } - - .info-section:hover { - background: rgba(255, 255, 255, 0.04); - transform: translateY(-1px); - } - - .section-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-weight: 500; - border-radius: 8px; - margin: 3px; - position: relative; - overflow: hidden; - } - - .section-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); - transform: translateX(-100%); - transition: transform 0.6s ease; - } - - .section-header:hover::before { - transform: translateX(100%); - } - - .section-header:hover { - background: rgba(255, 255, 255, 0.08); - transform: translateX(2px); - box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); - } - - .section-header:active { - background: rgba(255, 255, 255, 0.12); - transform: translateX(1px); - } - - .section-header.expanded { - background: rgba(116, 185, 255, 0.1); - border-left: 2px solid var(--info-panel-accent-color, #74b9ff); - } - - .info-section[data-section="node"] { - background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); - border: 1px solid rgba(116, 185, 255, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="node"] .section-header { - background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); - border-radius: 8px 8px 0 0; - cursor: default; - } - - .info-section[data-section="node"] .section-header:hover { - transform: none; - box-shadow: none; - } - - .info-section[data-section="node"] .section-title { - color: var(--info-panel-accent-color, #74b9ff); - font-weight: 700; - } - - .info-section[data-section="inspector"] { - background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); - border: 1px solid rgba(100, 150, 255, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="inspector"] .section-header { - background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); - border-radius: 8px 8px 0 0; - } - - .info-section[data-section="inspector"] .section-title { - color: #6496ff; - font-weight: 700; - } - - .info-section[data-section="media"] { - background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); - border: 1px solid rgba(253, 203, 110, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="media"] .section-header { - background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); - border-radius: 8px 8px 0 0; - } - - .info-section[data-section="media"] .section-title { - color: var(--info-panel-accent-color, #fdcb6e); - font-weight: 700; - } - - .section-icon { - font-size: 18px; - width: 22px; - text-align: center; - } - - .section-title { - flex: 1; - font-size: 16px; - font-weight: 600; - letter-spacing: 0.3px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - } - - .section-badge { - background: rgba(100, 150, 255, 0.2); - color: #8bb3ff; - padding: 3px 10px; - border-radius: 10px; - font-size: 13px; - font-weight: 500; - } - - .expand-icon { - font-size: 14px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - color: rgba(255, 255, 255, 0.6); - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: rgba(255, 255, 255, 0.05); - margin-left: auto; - } - - .section-header:hover .expand-icon { - color: var(--info-panel-accent-color, #74b9ff); - background: rgba(116, 185, 255, 0.15); - transform: scale(1.1); - } - - .section-header.expanded .expand-icon { - transform: rotate(90deg) scale(1.1); - color: var(--info-panel-accent-color, #74b9ff); - background: rgba(116, 185, 255, 0.2); - } - - .section-content { - max-height: 0; - overflow: visible; - transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; - opacity: 0; - } - - .section-content.expanded { - max-height: 2000px; - opacity: 1; - overflow: visible; - } - - .section-body { - padding: 0 18px 16px 24px; - } - - .info-row { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: 10px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; - border-radius: 4px; - margin: 0 -8px; - padding-left: 8px; - padding-right: 8px; - } - - .info-row:last-child { - border-bottom: none; - } - - .info-row:hover { - background: rgba(255, 255, 255, 0.03); - border-bottom-color: rgba(255, 255, 255, 0.08); - } - - .info-label { - font-weight: 500; - opacity: 0.85; - font-size: 15px; - min-width: 80px; - padding-right: 12px; - line-height: 1.5; - } - - .info-value { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - color: var(--info-panel-accent-color, #a0d468); - background: rgba(160, 212, 104, 0.12); - padding: 4px 10px; - border-radius: 6px; - max-width: 100%; - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - line-height: 1.4; - text-align: left; - white-space: pre-wrap; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - } - - .info-value:hover { - background: rgba(160, 212, 104, 0.18); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - } - - .info-value.long-text { - white-space: pre-wrap; - word-wrap: break-word; - max-width: 100%; - line-height: 1.4; - } - - .empty-state { - text-align: center; - padding: 50px 25px; - opacity: 0.6; - } - - .empty-state-icon { - font-size: 36px; - margin-bottom: 15px; - opacity: 0.5; - } - - /* Copy to clipboard functionality */ - .info-value[title]:hover::after { - content: '📋'; - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); - font-size: 12px; - opacity: 0.7; - } - - /* Smooth loading states */ - .section-body.loading { - opacity: 0.5; - pointer-events: none; - } - - .section-body.loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - border: 2px solid transparent; - border-top: 2px solid var(--info-panel-accent-color, #74b9ff); - border-radius: 50%; - animation: spin 1s linear infinite; - transform: translate(-50%, -50%); - } - - @keyframes spin { - to { transform: translate(-50%, -50%) rotate(360deg); } - } - - /* Enhanced scrollbar for better UX */ - .section-body::-webkit-scrollbar { - width: 4px; - } - - .section-body::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 2px; - } - - .section-body::-webkit-scrollbar-thumb { - background: var(--info-panel-accent-color, #74b9ff); - border-radius: 2px; - opacity: 0.6; - } - - .section-body::-webkit-scrollbar-thumb:hover { - opacity: 1; - } - - .empty-state-text { - font-size: 16px; - font-style: italic; - } - - @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } - } - - .pulse { - animation: pulse 2s infinite; - } - `; - } - - cleanup() { - if (this.elements.panel) { - this.elements.panel.remove(); - } - - if (this.elements.controls) { - this.elements.controls.remove(); - } - - const styles = document.getElementById('magnify-info-panel-styles-v2'); - if (styles) { - styles.remove(); - } - } - } - - /** - * Position Manager - * Handles all positioning logic including pinning and boundary checking - */ - class PositionManager { - constructor(stateManager, uiManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; - } - - positionPanel() { - if (!this.uiManager.elements.panel) return; - - // If panel is pinned, keep it at its pinned position - if (this.stateManager.state.isPanelPinned) { - this.applyPinnedPosition(); - this.positionFloatingControls(); - return; - } - - // Normal positioning logic - this.calculateNormalPosition(); - this.positionFloatingControls(); - } - - positionFloatingControls() { - if (!this.uiManager.elements.controls) return; - - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - - let idealX, idealY; - // Determine dimensions based on layout. Fallback values are estimates. - const isHorizontal = ['top', 'bottom'].includes(controlsPosition); - const controlsWidth = this.uiManager.elements.controls.offsetWidth || (isHorizontal ? 160 : 40); - const controlsHeight = this.uiManager.elements.controls.offsetHeight || (isHorizontal ? 40 : 160); - const margin = 5; - - // Determine the ideal, unconstrained position for the controls - switch (controlsPosition) { - case "top-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top; - break; - case "top-right": - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - case "bottom-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "bottom-right": - idealX = panelRect.right + margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "top": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.top - controlsHeight - margin; - break; - case "bottom": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.bottom + margin; - break; - case "left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - case "right": - idealX = panelRect.right + margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - default: // Fallback to top-right - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - } - - let finalX = idealX; - let finalY = idealY; - - // For side-positioned controls, check if they are forced to move horizontally. - // If so, override their vertical alignment to be "top" instead of "center". - if (['left', 'right'].includes(controlsPosition)) { - if (idealX < margin || (idealX + controlsWidth) > (window.innerWidth - margin)) { - finalY = panelRect.top; // Switch to top alignment - - // Also, place it slightly inside the panel to ensure it's visible. - if (controlsPosition === 'left') { - finalX = margin; - } else { // 'right' - finalX = window.innerWidth - controlsWidth - margin; - } - } - } - - // Apply boundary constraints to the final calculated position - finalX = Math.max(margin, Math.min(finalX, window.innerWidth - controlsWidth - margin)); - finalY = Math.max(margin, Math.min(finalY, window.innerHeight - controlsHeight - margin)); - - this.uiManager.elements.controls.style.left = `${finalX}px`; - this.uiManager.elements.controls.style.top = `${finalY}px`; - - // Update layout class based on the setting - this.uiManager.updateControlsLayout(controlsPosition); - } - - applyPinnedPosition() { - let { x, y } = this.stateManager.state.pinnedPosition; - const panel = this.uiManager.elements.panel; - const panelWidth = panel.offsetWidth; - const panelHeight = panel.offsetHeight; - const margin = 10; - - // Ensure the pinned panel stays within the viewport boundaries, e.g., after content expands. - // This adjustment is now temporary and won't update the stored pinned position. - const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); - - panel.style.left = `${boundedX}px`; - panel.style.top = `${boundedY}px`; - } - - calculateNormalPosition() { - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], this.uiManager.elements.panel.scrollHeight); - - let left, top; - const margin = 15; - - if (!this.stateManager.state.isGlassPreviewVisible) { - // Position at mouse cursor when glass is hidden - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } else { - // Position relative to magnify glass - const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); - if (glassRect) { - const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; - - switch (position) { - case "Right": - left = glassRect.right + margin; - top = glassRect.top; - break; - case "Left": - left = glassRect.left - panelWidth - margin; - top = glassRect.top; - break; - case "Top": - left = glassRect.left; - top = glassRect.top - panelHeight - margin; - break; - case "Bottom": - left = glassRect.left; - top = glassRect.bottom + margin; - break; - default: - left = glassRect.right + margin; - top = glassRect.top; - break; - } - } else { - // Fallback to mouse position - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } - } - - // Apply boundary constraints - left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); - top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); - - this.uiManager.elements.panel.style.left = `${left}px`; - this.uiManager.elements.panel.style.top = `${top}px`; - } - } - - /** - * Event Manager - * Handles all event binding and delegation - */ - class EventManager { - constructor(stateManager, uiManager, positionManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; - this.positionManager = positionManager; - - this.setupEventListeners(); - } - - setupEventListeners() { - this.setupPanelEvents(); - this.setupHotkeyEvents(); - this.setupDragEvents(); - this.setupHoverEvents(); - } - - setupPanelEvents() { - // Event delegation for panel controls - this.uiManager.elements.panel.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - - // Section header clicks - const sectionHeader = e.target.closest('.section-header'); - if (sectionHeader && sectionHeader.dataset.section) { - const sectionId = sectionHeader.dataset.section; - if (sectionId !== 'node') { // Node section is always expanded - this.stateManager.toggleSection(sectionId); - this.uiManager.updateSectionStates(); - } - } - }); - - // Event delegation for floating controls (separate since they're not children of panel) - if (this.uiManager.elements.controls) { - this.uiManager.elements.controls.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - }); - - // Prevent floating controls from propagating events - this.uiManager.elements.controls.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.controls.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); - } - - // Prevent panel interactions from propagating - this.uiManager.elements.panel.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.panel.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); - } - - // Helper function to check if user is typing in an input field - isUserTyping() { - const activeElement = document.activeElement; - if (!activeElement) return false; - - // Check if the active element is an input, textarea, or contenteditable - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea') { - return true; - } - - // Check for contenteditable elements - if (activeElement.contentEditable === 'true') { - return true; - } - - // Check if it's inside a form or has input-like classes - if (activeElement.closest('form') || - activeElement.classList.contains('cm-editor') || // CodeMirror editor - activeElement.classList.contains('monaco-editor') || // Monaco editor - activeElement.closest('.cm-editor') || - activeElement.closest('.monaco-editor')) { - return true; - } - - return false; - } - - setupHotkeyEvents() { - document.addEventListener('keydown', (e) => { - // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) - if (this.isUserTyping()) { - return; - } - - // Only handle hotkeys when magnifying glass is active - if (!magnifyGlass.state.active) return; - - const settings = this.stateManager.state.settings; - - // Toggle info panel - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-panel'); - } - - // Toggle glass preview - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-glass'); - } - - // Unlock panel from magnify glass and move to current mouse location (no Alt required due to Smart Input Detection) - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('pin-at-mouse'); - } - }); - } - - setupDragEvents() { - let dragState = { - isDragging: false, - startX: 0, - startY: 0, - currentX: 0, - currentY: 0, - offsetX: 0, - offsetY: 0 - }; - - // Start drag - const startDrag = (e) => { - // Only when pinned and not locked - if (!this.stateManager.state.isPanelPinned || this.stateManager.state.isPanelLocked) return; - - // Block on specific elements only - if (e.target.closest('.minimize-btn') || - e.target.closest('.control-btn') || - e.target.tagName === 'BUTTON') { - return; - } - - // console.log('[DRAG] Starting drag'); - - // Stop all event propagation immediately - e.preventDefault(); - e.stopImmediatePropagation(); - - // Set drag state - dragState.isDragging = true; - dragState.startX = e.clientX; - dragState.startY = e.clientY; - - // Get current panel position - const rect = this.uiManager.elements.panel.getBoundingClientRect(); - dragState.currentX = rect.left; - dragState.currentY = rect.top; - dragState.offsetX = e.clientX - rect.left; - dragState.offsetY = e.clientY - rect.top; - - // Visual feedback - this.uiManager.elements.panel.classList.add('panel-dragging'); - this.uiManager.elements.panel.style.cursor = 'grabbing'; - document.body.style.cursor = 'grabbing'; - document.body.style.userSelect = 'none'; - - // console.log('[DRAG] Drag started at:', { x: dragState.currentX, y: dragState.currentY }); - }; - - // Handle drag movement - const doDrag = (e) => { - if (!dragState.isDragging) return; - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate new position based on mouse movement - const newX = e.clientX - dragState.offsetX; - const newY = e.clientY - dragState.offsetY; - - // Apply boundaries - const panelWidth = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = this.uiManager.elements.panel.offsetHeight; - const margin = 10; - - const boundedX = Math.max(margin, Math.min(newX, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(newY, window.innerHeight - panelHeight - margin)); - - // Apply position immediately and forcefully - this.uiManager.elements.panel.style.position = 'fixed'; - this.uiManager.elements.panel.style.left = `${boundedX}px`; - this.uiManager.elements.panel.style.top = `${boundedY}px`; - this.uiManager.elements.panel.style.transform = 'none'; - this.uiManager.elements.panel.style.zIndex = '10010'; - - // Update current position - dragState.currentX = boundedX; - dragState.currentY = boundedY; - - // Update floating controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - - // console.log('[DRAG] Moving to:', { x: boundedX, y: boundedY }); - }; - - // End drag - const endDrag = (e) => { - if (!dragState.isDragging) return; - - // console.log('[DRAG] Ending drag'); - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate if we actually moved significantly - const deltaX = Math.abs(e.clientX - dragState.startX); - const deltaY = Math.abs(e.clientY - dragState.startY); - const moved = deltaX > 5 || deltaY > 5; - - if (moved) { - // Save the final position - this.stateManager.setPinnedPosition(dragState.currentX, dragState.currentY); - // console.log('[DRAG] Position saved:', { x: dragState.currentX, y: dragState.currentY }); - } - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - - // Cancel drag - const cancelDrag = () => { - if (!dragState.isDragging) return; - - // console.log('[DRAG] Cancelling drag'); - - // Restore original position from pinned state - const { x, y } = this.stateManager.state.pinnedPosition; - this.uiManager.elements.panel.style.left = `${x}px`; - this.uiManager.elements.panel.style.top = `${y}px`; - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - - // Update controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - }; - - // Attach events with capture to prevent interference - this.uiManager.elements.panel.addEventListener('mousedown', startDrag, { capture: true, passive: false }); - document.addEventListener('mousemove', doDrag, { capture: true, passive: false }); - document.addEventListener('mouseup', endDrag, { capture: true, passive: false }); - - // Handle escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - cancelDrag(); - } - }); - - // Handle mouse leaving window - document.addEventListener('mouseleave', endDrag); - - // Visual feedback - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - if (this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked && !dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = 'grab'; - } - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - if (!dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = ''; - } - }); - - // console.log('[DRAG] Event listeners attached with capture mode'); - - // Store cleanup function - this.dragCleanup = () => { - this.uiManager.elements.panel.removeEventListener('mousedown', startDrag, { capture: true }); - document.removeEventListener('mousemove', doDrag, { capture: true }); - document.removeEventListener('mouseup', endDrag, { capture: true }); - document.removeEventListener('mouseleave', endDrag); - cancelDrag(); - }; - } - - setupHoverEvents() { - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - this.stateManager.state.isPanelHovered = true; - this.stateManager.clearAutoExpandTimer(); - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - this.stateManager.state.isPanelHovered = false; - if (!this.stateManager.state.isHoveringNode) { - this.stateManager.scheduleAutoCollapse(); - } - }); - } - - captureCurrentPanelPosition() { - // Capture current position when pinning is activated - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - this.stateManager.setPinnedPosition(panelRect.left, panelRect.top); - // console.log(`Panel pinned at current position: (${panelRect.left}, ${panelRect.top})`); - } - - handleControlAction(action) { - switch (action) { - case 'minimize': - this.stateManager.toggleMinimized(); - this.uiManager.updateMinimizedState(); - break; - - case 'pin': - // If not currently pinned, either restore to last position or capture current - if (!this.stateManager.state.isPanelPinned) { - // If we don't have a remembered position, capture current position - if (!this.stateManager.state.lastPinnedPosition) { - this.captureCurrentPanelPosition(); - } - // Note: togglePinning will restore lastPinnedPosition if it exists - } - - this.stateManager.togglePinning(); - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - break; - - case 'lock': - this.stateManager.toggleLocking(); - this.uiManager.updatePinnedState(); // This updates both pinned and locked states - break; - - case 'pin-at-mouse': - // Pin at current mouse location (Alt + configured key) - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], 400); - - // Position panel near mouse with some offset to avoid covering cursor area - const mouseX = magnifyGlass.lastKnownMousePosition.x; - const mouseY = magnifyGlass.lastKnownMousePosition.y; - const pinKey = settings["🔍MagnifyGlass.PinPanelHotkey"]; - - let pinX = mouseX + 50; // Offset to the right of mouse - let pinY = mouseY - 100; // Offset above mouse - - // Apply boundary constraints - pinX = Math.max(10, Math.min(pinX, window.innerWidth - panelWidth - 10)); - pinY = Math.max(10, Math.min(pinY, window.innerHeight - panelHeight - 10)); - - // Set the new pin position (this will also save as lastPinnedPosition) - this.stateManager.setPinnedPosition(pinX, pinY); - - // Enable pinning if not already pinned - if (!this.stateManager.state.isPanelPinned) { - this.stateManager.state.isPanelPinned = true; - } - - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - // console.log(`Panel pinned at mouse location: (${pinX}, ${pinY}) using Alt+${pinKey.toUpperCase()}`); - break; - - case 'toggle-panel': - if (this.stateManager.togglePanelVisibility()) { - this.uiManager.show(); - } else { - this.uiManager.hide(); - } - this.uiManager.updateControlStates(); - break; - - case 'toggle-glass': - const isVisible = this.stateManager.toggleGlassPreview(); - this.applyGlassVisibility(isVisible); - - // When the glass preview is hidden, automatically pin the panel at its current location. - if (!isVisible && !this.stateManager.state.isPanelPinned) { - this.captureCurrentPanelPosition(); // Capture current position before pinning - this.stateManager.togglePinning(); - this.stateManager.state.isAutoPinned = true; // Mark as auto-pinned - this.uiManager.updatePinnedState(); - } - - // When the glass preview is shown again, only auto-unlock if NOT manually locked with 📌 button - if (isVisible && this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked) { - this.stateManager.togglePinning(); // Unpin the panel - this.stateManager.state.isAutoPinned = false; // Clear auto-pin flag - this.uiManager.updatePinnedState(); - } - - this.uiManager.updateControlStates(); - this.positionManager.positionPanel(); // Reposition based on new glass state - break; - } - } - - applyGlassVisibility(isVisible) { - if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { - magnifyGlass.ui.glassDiv.style.opacity = isVisible ? "1" : "0"; - } - } - } - - /** - * Information Gatherer - * Responsible for collecting information about the current state - */ - class InformationGatherer { - constructor() {} - - gatherInformation() { - const info = { - timestamp: Date.now(), - cursor: { - canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, - screen: { - x: magnifyGlass.lastKnownMousePosition.x, - y: magnifyGlass.lastKnownMousePosition.y - } - }, - canvas: { - scale: magnifyGlass.state.canvasScale, - offset: { - x: magnifyGlass.state.canvasOffsetX, - y: magnifyGlass.state.canvasOffsetY - } - }, - magnifier: { - zoomFactor: magnifyGlass.config.zoomFactor, - offsetX: magnifyGlass.config.offsetX, - offsetY: magnifyGlass.config.offsetY, - sourceRegion: { - x: magnifyGlass.state.sourceX, - y: magnifyGlass.state.sourceY, - width: magnifyGlass.state.sourceWidth, - height: magnifyGlass.state.sourceHeight - } - }, - hoveredNode: null, - node: null, - widget: null, - connection: null, - media: null - }; - - if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { - info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); - } - - const nodeUnderCursor = this.getNodeUnderCursor(); - if (nodeUnderCursor) { - info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); - info.node = this.getNodeInfo(nodeUnderCursor.node); - - const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); - if (widget) { - info.widget = this.getWidgetInfo(widget); - } - } - - return info; - } - - getNodeUnderCursor() { - if (!app.graph || !app.canvas) { - return null; - } - - try { - // Try to get the node from ComfyUI's state - if (app.canvas.node_over) { - const node = app.canvas.node_over; - if (node && node.pos && node.size) { - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } - } - - // Fallback to manual detection - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - for (let i = app.graph._nodes.length - 1; i >= 0; i--) { - const node = app.graph._nodes[i]; - if (!node.pos || !node.size || node.flags?.collapsed) continue; - - if (graphPos[0] >= node.pos[0] && - graphPos[0] <= node.pos[0] + node.size[0] && - graphPos[1] >= node.pos[1] && - graphPos[1] <= node.pos[1] + node.size[1]) { - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } - } - } catch (err) { - console.warn("Error in node detection:", err); - } - - return null; - } - - getDetailedNodeInfo(node, localPos) { - return { - id: node.id, - title: node.title || "Untitled Node", - type: node.type, - mode: this.getNodeModeText(node.mode), - position: { - x: Math.round(node.pos[0]), - y: Math.round(node.pos[1]), - formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` - }, - size: { - width: Math.round(node.size[0]), - height: Math.round(node.size[1]), - formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` - }, - localPosition: { - x: Math.round(localPos.x), - y: Math.round(localPos.y), - formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, - percentage: { - x: ((localPos.x / node.size[0]) * 100).toFixed(1), - y: ((localPos.y / node.size[1]) * 100).toFixed(1) - } - }, - counts: { - widgets: node.widgets ? node.widgets.length : 0, - inputs: node.inputs ? node.inputs.length : 0, - outputs: node.outputs ? node.outputs.length : 0, - properties: node.properties ? Object.keys(node.properties).length : 0 - }, - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {}, - hoverRegion: this.detectNodeRegion(localPos, node) - }; - } - - getNodeInfo(node) { - return { - id: node.id, - title: node.title || "Untitled", - type: node.type, - mode: node.mode, - size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", - position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {} - }; - } - - getWidgetUnderCursor(node, localPos) { - if (!node.widgets || !node.widgets.length) return null; - - const titleHeight = 30; - let currentY = titleHeight; - - for (const widget of node.widgets) { - const widgetHeight = 25; - - if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { - return widget; - } - - currentY += widgetHeight + 5; - } - - return null; - } - - getWidgetInfo(widget) { - return { - name: widget.name, - type: widget.type, - value: this.formatValue(widget.value), - options: widget.options || null, - min: widget.min, - max: widget.max, - step: widget.step - }; - } - - getMediaInfo(mediaElement) { - const info = { - tagName: mediaElement.tagName, - src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" - }; - - if (mediaElement.tagName === 'IMG') { - info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; - info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; - info.complete = mediaElement.complete; - } else if (mediaElement.tagName === 'VIDEO') { - info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; - info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; - info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; - info.paused = mediaElement.paused; - info.readyState = mediaElement.readyState; - } - - return info; - } - - getNodeModeText(mode) { - const modes = { - 0: "Always", - 1: "On Event", - 2: "Never", - 3: "On Trigger", - 4: "On Request" - }; - return modes[mode] || `Mode ${mode}`; - } - - detectNodeRegion(localPos, node) { - const titleHeight = 30; - const regions = []; - - if (localPos.y <= titleHeight) { - regions.push("Title Bar"); - } - - if (localPos.x <= 10) { - regions.push("Left Edge"); - } else if (localPos.x >= node.size[0] - 10) { - regions.push("Right Edge"); - } - - if (localPos.y <= 10) { - regions.push("Top Edge"); - } else if (localPos.y >= node.size[1] - 10) { - regions.push("Bottom Edge"); - } - - if (regions.length === 0) { - if (localPos.y > titleHeight) { - regions.push("Content Area"); - } - } - - if (localPos.x <= 20 && localPos.y > titleHeight) { - regions.push("Input Area"); - } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { - regions.push("Output Area"); - } - - return regions.length > 0 ? regions.join(", ") : "Unknown"; - } - - formatValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); - } - } - - /** - * Main Professional Info Panel Manager - * Orchestrates all the other components - */ - class ProfessionalInfoPanelManager { - constructor(magnifyGlass) { - this.magnifyGlass = magnifyGlass; - - // Initialize all managers - this.stateManager = new StateManager(); - this.uiManager = new UIManager(this.stateManager); - this.positionManager = new PositionManager(this.stateManager, this.uiManager); - this.eventManager = new EventManager(this.stateManager, this.uiManager, this.positionManager); - this.informationGatherer = new InformationGatherer(); - - this.hookIntoMagnifyGlass(); - - console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); - } - - hookIntoMagnifyGlass() { - // Hook into magnify glass update cycle - const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); - - this.magnifyGlass.updateMagnifiedView = (() => { - originalUpdateMagnifiedView(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.scheduleInfoUpdate(); - } - }).bind(this); - - // Hook into show/hide - const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); - const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); - - this.magnifyGlass.ui.show = (() => { - originalShow(); - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.uiManager.show(); - // Position controls after showing - setTimeout(() => this.positionManager.positionPanel(), 10); - } - }).bind(this); - - this.magnifyGlass.ui.hide = (() => { - originalHide(); - this.uiManager.hide(); - }).bind(this); - } - - scheduleInfoUpdate() { - if (this.stateManager.state.updateScheduled) return; - - this.stateManager.state.updateScheduled = true; - requestAnimationFrame(() => { - this.updateInfo(); - this.stateManager.state.updateScheduled = false; - }); - } - - updateInfo() { - if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; - - const info = this.informationGatherer.gatherInformation(); - this.stateManager.setCurrentInfo(info); - this.uiManager.displayInfo(info); - this.positionManager.positionPanel(); - - // Handle node hover state - if (info.hoveredNode) { - this.stateManager.state.isHoveringNode = true; - this.stateManager.expandNodeSections(); - - if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { - this.stateManager.state.lastNodeId = info.hoveredNode.id; - } - } else { - this.stateManager.state.isHoveringNode = false; - - if (this.stateManager.state.lastNodeId !== null) { - this.stateManager.state.lastNodeId = null; - if (!this.stateManager.state.isPanelHovered) { - this.stateManager.scheduleAutoCollapse(); - } - } - } - } - - updateSettings() { - const changes = this.stateManager.updateSettings(); - - // React to setting changes (theme is now auto-detected) - // Theme changes are handled automatically by the observer - - if (changes["🔍MagnifyGlass.ControlsPosition"]) { - // Update controls layout and position when position setting changes - this.positionManager.positionPanel(); - } - - // Apply new styles - this.uiManager.applyStyles(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.uiManager.show(); - } else { - this.uiManager.hide(); - } - } - - cleanup() { - this.stateManager.cleanup(); - this.uiManager.cleanup(); - } - } - - // Create the main manager instance - const infoPanelManager = new ProfessionalInfoPanelManager(magnifyGlass); - - // Store reference globally for cleanup and theme updates - if (!window.comfyUIMagnifyGlassExtensions) { - window.comfyUIMagnifyGlassExtensions = []; - } - window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); - - // Store main manager reference for theme updates - window.infoPanelManager = infoPanelManager; - - // Register settings with ComfyUI - Object.keys(DEFAULT_SETTINGS).forEach(settingKey => { - const settingConfig = getSettingConfig(settingKey, DEFAULT_SETTINGS[settingKey]); - if (settingConfig) { - app.ui.settings.addSetting({ - ...settingConfig, - onChange: (value) => { - infoPanelManager.updateSettings(); - } - }); - } - }); - - function getSettingConfig(key, defaultValue) { - const configs = { - "🔍MagnifyGlass.InfoPanelEnabled": { - id: key, - name: "📋 Magnify Glass: Info Panel", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable or disable the professional information panel." - }, - "🔍MagnifyGlass.InfoPanelPosition": { - id: key, - name: "📋 Magnify Glass: Info Panel Position", - type: "combo", - options: [ - { value: "Right", text: "Right" }, - { value: "Left", text: "Left" }, - { value: "Top", text: "Top" }, - { value: "Bottom", text: "Bottom" } - ], - defaultValue, - tooltip: "Position of the info panel relative to the magnifying glass." - }, - "🔍MagnifyGlass.InfoPanelWidth": { - id: key, - name: "📋 Magnify Glass: Info Panel Width", - type: "slider", - defaultValue, - min: 250, - max: 450, - step: 10, - tooltip: "Width of the information panel in pixels." - }, - "🔍MagnifyGlass.InfoPanelOpacity": { - id: key, - name: "📋 Magnify Glass: Info Panel Opacity (%)", - type: "slider", - defaultValue, - min: 10, - max: 100, - step: 5, - tooltip: "Opacity of the information panel background as a percentage (10 = very transparent, 100 = fully opaque).", - onChange: (value) => { - // Convert percentage to decimal and apply opacity immediately - const opacity = value / 100; - if (window.infoPanelManager && window.infoPanelManager.uiManager && window.infoPanelManager.uiManager.elements.panel) { - window.infoPanelManager.uiManager.elements.panel.style.opacity = opacity; - } - } - }, - "🔍MagnifyGlass.InfoPanelMaxHeight": { - id: key, - name: "📋 Magnify Glass: Info Panel Max Height", - type: "slider", - defaultValue, - min: 300, - max: 700, - step: 25, - tooltip: "Maximum height of the information panel in pixels." - }, - - "🔍MagnifyGlass.InfoPanelAnimations": { - id: key, - name: "🎬 Magnify Glass: Info Panel Animations", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable or disable animations for the info panel." - }, - "🔍MagnifyGlass.ShowInspectorTab": { - id: key, - name: "📋 Magnify Glass: Show Inspector Tab", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Show or hide the Inspector tab with cursor and canvas information." - }, - "🔍MagnifyGlass.ToggleHotkey": { - id: key, - name: "🔑 Magnify Glass: Info Panel Toggle Hotkey", - type: "combo", - options: ["i", "p", "o", "l", "k"], - defaultValue, - tooltip: "Key to toggle the info panel visibility while the magnifier is active." - }, - "🔍MagnifyGlass.GlassPreviewToggleHotkey": { - id: key, - name: "🔑 Magnify Glass: Preview Toggle Hotkey", - type: "combo", - options: ["g", "v", "b", "m", "n"], - defaultValue, - tooltip: "Key to toggle the magnifying glass preview visibility." - }, - "🔍MagnifyGlass.PinPanelHotkey": { - id: key, - name: "🔓 Magnify Glass: Unlock Panel to Mouse Location", - type: "combo", - options: ["u", "p", "f", "x", "z"], - defaultValue, - tooltip: "Key to unlock the info panel from the magnify glass and move it to your current mouse position (e.g., U). Panel remains movable - use the pin button to lock it in place. Alt not required due to Smart Input Detection." - }, - "🔍MagnifyGlass.ShowHoveringControls": { - id: key, - name: "🎮 Magnify Glass: Show Hover Controls", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Show or hide hovering UI controls above the info panel." - }, - "🔍MagnifyGlass.ControlsPosition": { - id: key, - name: "📍 Magnify Glass: Controls Position", - type: "combo", - options: [ - { value: "top-left", text: "Top-Left" }, - { value: "top-right", text: "Top-Right" }, - { value: "bottom-left", text: "Bottom-Left" }, - { value: "bottom-right", text: "Bottom-Right" }, - { value: "top", text: "Top (Centered)" }, - { value: "bottom", text: "Bottom (Centered)" }, - { value: "left", text: "Left (Centered)" }, - { value: "right", text: "Right (Centered)" } - ], - defaultValue, - tooltip: "Position of the floating control buttons relative to the info panel." - }, - "🔍MagnifyGlass.InfoPanelTextColor": { - id: key, - name: "🎨 Magnify Glass: Info Panel Text Color", - type: "color", - defaultValue, - tooltip: "Color of the text in the info panel. This affects all text content including titles, values, and labels.", - onChange: (value) => { - // Ensure color value includes # symbol for valid CSS and persistence - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - - // Only save back if we actually normalized the color (to prevent recursion) - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized text color:', e); - } - } - - // Apply the color immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.applyStyles(); - } - } - }, - "🔍MagnifyGlass.InfoPanelAccentColor": { - id: key, - name: "🎨 Magnify Glass: Info Panel Accent Color", - type: "color", - defaultValue, - tooltip: "Color used for highlighted elements like values, section titles, and active states in the info panel.", - onChange: (value) => { - // Ensure color value includes # symbol for valid CSS and persistence - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - - // Only save back if we actually normalized the color (to prevent recursion) - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized accent color:', e); - } - } - - // Apply the color immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.applyStyles(); - } - } + setTimeout(check, 100); } }; - - return configs[key] || null; - } - - console.log("ComfyUI Magnify Info Panel Pro V2: All settings registered and initialized"); + check(); + }); + }; + + // Get magnify glass instance + const magnifyGlass = await checkForMagnifyGlass(); + + // Create info panel manager + const infoPanelManager = new InfoPanel(magnifyGlass); + + // Store reference globally for cleanup and theme updates + if (!window.comfyUIMagnifyGlassExtensions) { + window.comfyUIMagnifyGlassExtensions = []; } + window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); + + // Store main manager reference for theme updates + window.infoPanelManager = infoPanelManager; + + // Register settings with ComfyUI + Object.keys(DEFAULT_PANEL_SETTINGS).forEach(settingKey => { + const settingConfig = getSettingConfig(settingKey, DEFAULT_PANEL_SETTINGS[settingKey]); + if (settingConfig) { + app.ui.settings.addSetting({ + ...settingConfig, + onChange: (value) => { + infoPanelManager.updateSettings(); + } + }); + } + }); + + console.log("ComfyUI Magnify Info Panel (Refactored): Loaded successfully"); } -}); \ No newline at end of file +}); diff --git a/web/magnify_info_panel.js.backup b/web/magnify_info_panel.js.backup new file mode 100644 index 0000000..58cc47f --- /dev/null +++ b/web/magnify_info_panel.js.backup @@ -0,0 +1,3035 @@ +import { app } from "../../../scripts/app.js"; + +/** + * ComfyUI Magnifying Glass - Professional Information Panel Extension (Refactored) + * + * This script provides a professional, highly interactive information panel that displays + * detailed analysis of whatever you're hovering over when the magnifying glass is active. + * + * Key Features: + * - Unified state management + * - Smooth mode transitions + * - Improved pin functionality with drag support + * - Lock feature to prevent dragging when pinned + * - Clean control system + * - Better positioning logic + */ + +app.registerExtension({ + name: "comfyui.magnify.glass.info.panel.pro.v2", + async setup() { + // Wait for the main magnifying glass to be ready + let magnifyGlass = null; + let retryCount = 0; + const maxRetries = 50; + + const waitForMagnifyGlass = () => { + if (window.comfyUIMagnifyGlass) { + magnifyGlass = window.comfyUIMagnifyGlass; + console.log("ComfyUI Magnify Info Panel Pro V2: Found magnifying glass instance"); + initializeInfoPanel(); + } else if (retryCount < maxRetries) { + retryCount++; + setTimeout(waitForMagnifyGlass, 100); + } else { + console.warn("ComfyUI Magnify Info Panel Pro V2: Could not find magnifying glass instance after 5 seconds"); + } + }; + + setTimeout(waitForMagnifyGlass, 100); + + function initializeInfoPanel() { + // Default settings configuration + const DEFAULT_SETTINGS = { + "🔍MagnifyGlass.InfoPanelEnabled": true, + "🔍MagnifyGlass.InfoPanelPosition": "Bottom", + "🔍MagnifyGlass.InfoPanelWidth": 320, + "🔍MagnifyGlass.InfoPanelOpacity": 100, + "🔍MagnifyGlass.InfoPanelMaxHeight": 1000, + "🔍MagnifyGlass.InfoPanelAnimations": false, + "🔍MagnifyGlass.ShowInspectorTab": false, + "🔍MagnifyGlass.ToggleHotkey": "i", + "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.PinPanelHotkey": "u", + "🔍MagnifyGlass.ShowHoveringControls": true, + "🔍MagnifyGlass.ControlsPosition": "bottom", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", + }; + + const getSettingValue = (key, defaultValue) => { + try { + const value = app.ui.settings.getSettingValue(key); + return value === undefined ? defaultValue : value; + } catch (e) { + console.warn(`ComfyUI Magnify Info Panel Pro V2: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } + }; + + /** + * Centralized State Manager + * Handles all state-related operations and provides clean state transitions + */ + class StateManager { + constructor() { + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + isPanelMinimized: false, + isPanelPinned: false, + isPanelLocked: false, // New lock state + isAutoPinned: false, // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: true, + + // Section expansion + expandedSections: new Set(['node']), + + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + + // Current data + currentInfo: {}, + + // Settings cache + settings: {}, + + // Auto-detected theme + currentTheme: 'dark' // Will be auto-detected + }; + + this.initThemeDetection(); + this.loadSettings(); + } + + initThemeDetection() { + // Immediate forced detection + // console.log('ComfyUI Magnify Glass: Starting theme detection...'); + + // Detect ComfyUI's current theme + this.detectCurrentTheme(); + + // Force an immediate check in case we missed the initial state + setTimeout(() => { + // console.log('ComfyUI Magnify Glass: Delayed theme check...'); + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1000); + + // Set up mutation observer to watch for theme changes + this.setupThemeObserver(); + + // Listen for system theme changes as fallback + if (window.matchMedia) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + prefersDark.addListener(() => this.detectCurrentTheme()); + } + + // Add click listener to theme toggle buttons for immediate detection + this.setupThemeButtonListeners(); + } + + detectCurrentTheme() { + let detectedTheme = 'dark'; // Default fallback + + const body = document.body; + const html = document.documentElement; + const vueApp = document.querySelector('#vue-app'); + const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); + + // Get all possible theme-related elements + const allElements = [body, html, vueApp, sidebar].filter(Boolean); + + // Debug: Log current classes and attributes with more detail + const debugInfo = { + bodyClasses: Array.from(body.classList), + htmlClasses: Array.from(html.classList), + bodyDataTheme: body.getAttribute('data-theme'), + htmlDataTheme: html.getAttribute('data-theme'), + vueAppClasses: vueApp ? Array.from(vueApp.classList) : null, + sidebarClasses: sidebar ? Array.from(sidebar.classList) : null, + bodyBgColor: window.getComputedStyle(body).backgroundColor, + htmlBgColor: window.getComputedStyle(html).backgroundColor, + allComputedVars: {} + }; + + // Check CSS variables + const rootStyles = window.getComputedStyle(html); + const bodyStyles = window.getComputedStyle(body); + + // Get all CSS custom properties + for (let i = 0; i < rootStyles.length; i++) { + const prop = rootStyles[i]; + if (prop.startsWith('--')) { + debugInfo.allComputedVars[prop] = rootStyles.getPropertyValue(prop).trim(); + } + } + + // console.log('ComfyUI Magnify Glass: Comprehensive theme detection debug:', debugInfo); + + // Method 1: Look for specific light theme indicators in CSS variables + const primarySurface = rootStyles.getPropertyValue('--p-primary-color') || + rootStyles.getPropertyValue('--primary-color') || + bodyStyles.getPropertyValue('--p-primary-color') || + bodyStyles.getPropertyValue('--primary-color'); + + const surfaceGround = rootStyles.getPropertyValue('--p-surface-ground') || + rootStyles.getPropertyValue('--surface-ground') || + bodyStyles.getPropertyValue('--p-surface-ground') || + bodyStyles.getPropertyValue('--surface-ground'); + + const textColor = rootStyles.getPropertyValue('--p-text-color') || + rootStyles.getPropertyValue('--text-color') || + bodyStyles.getPropertyValue('--p-text-color') || + bodyStyles.getPropertyValue('--text-color'); + + // console.log('ComfyUI Magnify Glass: CSS Variables:', { primarySurface, surfaceGround, textColor }); + + // Method 2: Analyze background colors of multiple elements + const backgrounds = allElements.map(el => { + const styles = window.getComputedStyle(el); + return { + element: el.tagName || el.className, + backgroundColor: styles.backgroundColor, + color: styles.color + }; + }); + + // console.log('ComfyUI Magnify Glass: Background analysis:', backgrounds); + + // Method 3: Check for light backgrounds + for (const bg of backgrounds) { + if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { + const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + // console.log(`ComfyUI Magnify Glass: Element ${bg.element} brightness: ${brightness}`); + if (brightness > 180) { // Higher threshold for light detection + detectedTheme = 'light'; + // console.log(`ComfyUI Magnify Glass: Light theme detected via background analysis of ${bg.element}`); + break; + } + } + } + } + + // Method 4: Check for theme toggle buttons state (they might indicate current theme) + const themeButtons = document.querySelectorAll('[class*="theme"], button[title*="theme"], button[title*="Theme"], .p-button[title*="light"], .p-button[title*="Light"]'); + themeButtons.forEach(btn => { + // console.log('ComfyUI Magnify Glass: Found theme button:', btn); + }); + + // Method 5: Force light theme detection if we see light backgrounds + if (detectedTheme === 'dark') { + // Look for any element with very light background + const lightElements = document.querySelectorAll('*'); + for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements + const el = lightElements[i]; + const styles = window.getComputedStyle(el); + const bgColor = styles.backgroundColor; + + if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + if (brightness > 200) { // Very light elements + detectedTheme = 'light'; + // console.log(`ComfyUI Magnify Glass: Light theme forced via element ${el.tagName}.${el.className} with brightness ${brightness}`); + break; + } + } + } + } + } + + // Update theme if it changed + if (this.state.currentTheme !== detectedTheme) { + // console.log(`ComfyUI Magnify Glass: Theme changed from ${this.state.currentTheme} to ${detectedTheme}`); + this.state.currentTheme = detectedTheme; + return true; // Theme changed + } + + // console.log(`ComfyUI Magnify Glass: Current theme remains: ${detectedTheme}`); + return false; // No change + } + + setupThemeObserver() { + // Watch for changes to body/html classes and attributes + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + (mutation.attributeName === 'class' || + mutation.attributeName === 'data-theme' || + mutation.attributeName === 'style')) { + shouldCheck = true; + // console.log('ComfyUI Magnify Glass: DOM change detected:', { + // target: mutation.target.tagName, + // attribute: mutation.attributeName, + // oldValue: mutation.oldValue, + // newValue: mutation.target.getAttribute(mutation.attributeName) + // }); + } + }); + + if (shouldCheck && this.detectCurrentTheme()) { + // Theme changed, update UI immediately + this.notifyThemeChange(); + } + }); + + // Observe changes to body, html, and vue app + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + + const vueApp = document.querySelector('#vue-app'); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + + // Also watch for changes to any element with theme-related classes + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach(el => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + + // Periodic fallback check every 2 seconds + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2000); + } + + setupThemeButtonListeners() { + // Listen for clicks on theme toggle buttons + document.addEventListener('click', (e) => { + // Check if clicked element might be a theme toggle + const target = e.target; + const isThemeButton = target.textContent?.toLowerCase().includes('theme') || + target.textContent?.toLowerCase().includes('light') || + target.textContent?.toLowerCase().includes('dark') || + target.title?.toLowerCase().includes('theme') || + target.className?.toLowerCase().includes('theme'); + + if (isThemeButton) { + // console.log('ComfyUI Magnify Glass: Theme button clicked, triggering detection'); + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + + notifyThemeChange() { + // Update settings to reflect new theme + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + + // Notify UI components of theme change immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); + } + } + + loadSettings() { + Object.keys(DEFAULT_SETTINGS).forEach(key => { + // Skip the theme setting since we auto-detect it + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); + } + }); + + // Set theme to auto-detected value + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + } + + updateSettings() { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + + // Return what changed for reactive updates + const changes = {}; + Object.keys(this.state.settings).forEach(key => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] + }; + } + }); + + return changes; + } + + togglePanelVisibility() { + this.state.isPanelVisible = !this.state.isPanelVisible; + // console.log(`Info panel visibility toggled to ${this.state.isPanelVisible ? 'visible' : 'hidden'}`); + return this.state.isPanelVisible; + } + + toggleGlassPreview() { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + // console.log(`Glass preview visibility toggled to ${this.state.isGlassPreviewVisible ? 'visible' : 'hidden'}`); + return this.state.isGlassPreviewVisible; + } + + togglePinning() { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + + // When unpinning, also unlock and clear auto-pin flag + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning + } + + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + // Restore to last pinned position + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + // console.log(`Panel pinned at remembered position: (${this.state.pinnedPosition.x}, ${this.state.pinnedPosition.y})`); + } else if (!this.state.isPanelPinned && wasPinned) { + // Save current position when unpinning + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + // console.log(`Panel unpinned, position saved: (${this.state.lastPinnedPosition.x}, ${this.state.lastPinnedPosition.y})`); + } + + // console.log(`Panel ${this.state.isPanelPinned ? 'pinned' : 'unpinned'}`); + return this.state.isPanelPinned; + } + + toggleLocking() { + // Only allow locking when pinned + if (!this.state.isPanelPinned) { + // console.log("Cannot lock panel when not pinned"); + return false; + } + + this.state.isPanelLocked = !this.state.isPanelLocked; + // console.log(`Panel ${this.state.isPanelLocked ? 'locked' : 'unlocked'}`); + return this.state.isPanelLocked; + } + + toggleMinimized() { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + + toggleSection(sectionId) { + if (sectionId === 'node') return false; // Node section always expanded + + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + + setPinnedPosition(x, y) { + this.state.pinnedPosition = { x, y }; + // Also save as last pinned position for memory + this.state.lastPinnedPosition = { x, y }; + } + + setCurrentInfo(info) { + this.state.currentInfo = info; + } + + scheduleAutoCollapse() { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); + } + }, 1500); + } + } + + clearAutoExpandTimer() { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; + } + } + + expandNodeSections() { + this.state.expandedSections.add('hoveredNode'); + this.state.expandedSections.add('node'); + this.state.expandedSections.add('cursor'); + this.state.expandedSections.add('canvas'); + this.state.expandedSections.add('magnifier'); + this.clearAutoExpandTimer(); + } + + collapseNodeSections() { + this.state.expandedSections.delete('hoveredNode'); + this.state.expandedSections.delete('node'); + this.state.expandedSections.delete('widget'); + } + + cleanup() { + this.clearAutoExpandTimer(); + } + } + + /** + * UI Manager + * Handles all DOM manipulation and UI creation + */ + class UIManager { + constructor(stateManager) { + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + + this.createPanel(); + this.injectStyles(); + } + + createPanel() { + // Main panel container + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; + + // Header + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = ` +
+
🔍
+
Inspector
+
Real-time analysis
+
+
+ +
+ `; + + // Content container + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + + this.applyStyles(); + document.body.appendChild(this.elements.panel); + + // Create floating controls after panel is in DOM + if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { + this.createFloatingControls(); + // Update control states immediately after creation + this.updateControlStates(); + } + } + + createFloatingControls() { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical + this.elements.controls.innerHTML = ` + + + + + `; + + // Insert before the panel in the document body, not as a child + document.body.appendChild(this.elements.controls); + + // Set initial layout based on settings + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + this.updateControlsLayout(controlsPosition); + } + + updateControlStates() { + if (!this.elements.controls) return; + + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); + + if (pinBtn) { + pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; + } + + if (lockBtn) { + // Only show/enable lock button when pinned + lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; + lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; + } + + if (visibilityBtn) { + visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); + visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; + } + + if (glassBtn) { + glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); + glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; + } + } + + updateControlsLayout(position) { + if (!this.elements.controls) return; + + // Remove existing layout classes + this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); + + // Add appropriate layout class + if (['top', 'bottom'].includes(position)) { + this.elements.controls.classList.add('horizontal-layout'); + } else { + this.elements.controls.classList.add('vertical-layout'); + } + } + + applyStyles() { + const settings = this.stateManager.state.settings; + + // Apply custom text color via CSS variable if provided + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; + if (textColor) { + // Ensure color has # symbol + const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); + } + + // Apply custom accent color via CSS variable if provided + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (accentColor) { + // Ensure color has # symbol + const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); + } + + // Apply opacity setting if panel is visible (convert percentage to decimal) + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = opacityPercent / 100; + } + + this.elements.panel.style.cssText = ` + position: absolute; + width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; + max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; + z-index: 99999; + display: none; + opacity: 0; + transform: translateY(-10px); + transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; + pointer-events: auto; + user-select: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} + ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} + `; + } + + show() { + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + if (this.elements.controls) { + this.elements.controls.style.display = "flex"; + } + // Apply user's opacity setting when showing (convert percentage to decimal) + const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = opacityPercent / 100; + // Trigger reflow + this.elements.panel.offsetHeight; + this.elements.panel.classList.add('visible'); + this.stateManager.state.isPanelVisible = true; + } + } + + hide() { + this.elements.panel.classList.remove('visible'); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible) { + this.elements.panel.style.display = "none"; + if (this.elements.controls) { + this.elements.controls.style.display = "none"; + } + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + } + + updateMinimizedState() { + this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; + minimizeBtn.title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; + } + } + + updatePinnedState() { + this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + + updateTheme(newTheme) { + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + // console.log(`ComfyUI Magnify Glass: Panel theme updated to ${newTheme}`); + } + } + + displayInfo(info) { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + + buildSections(info) { + const sections = []; + const settings = this.stateManager.state.settings; + + // Inspector section + if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { + const inspectorContent = [ + { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } + ]; + + sections.push({ + id: 'inspector', + icon: '🔍', + title: 'Inspector', + content: inspectorContent + }); + } + + // Media section + if (info.media) { + const mediaContent = [ + { label: 'Type', value: info.media.tagName }, + { label: 'Source', value: info.media.src } + ]; + + if (info.media.naturalSize) { + mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); + } + + sections.push({ + id: 'media', + icon: '📷', + title: 'Media', + badge: info.media.tagName, + content: mediaContent + }); + } + + // Node Details section + if (info.hoveredNode) { + const nodeContent = [ + { label: 'Title', value: info.hoveredNode.title } + ]; + + // Check if this is a complex node that shows all widgets + const nodeType = info.hoveredNode.type.toLowerCase(); + const isSaveNode = nodeType.includes('save') && + !nodeType.includes('checkpoint') && + !nodeType.includes('model') && + !nodeType.includes('preview'); + + const showAllWidgets = info.hoveredNode.type && ( + nodeType.includes('ksampler') || + nodeType.includes('sampler') || + nodeType.includes('k_samplers') || + nodeType.includes('checkpoint') || + nodeType.includes('model') || + nodeType.includes('lora') || + nodeType.includes('controlnet') || + nodeType.includes('advanced') || + nodeType.includes('detailer') || + nodeType.includes('inpaint') || + nodeType.includes('upscale') || + nodeType.includes('clip') || + nodeType.includes('text') || + nodeType.includes('encode') + ) && !isSaveNode; + + if (!showAllWidgets) { + // Add specific extractions for simple nodes + const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); + if (checkpointInfo) { + nodeContent.push({ label: 'Model', value: checkpointInfo }); + } + + const imageInfo = this.getImageInfo(info.hoveredNode); + if (imageInfo) { + if (typeof imageInfo === 'string') { + nodeContent.push({ label: 'Image', value: imageInfo }); + } else if (typeof imageInfo === 'object') { + nodeContent.push({ label: 'Image Size', value: `${imageInfo.width}×${imageInfo.height}` }); + if (imageInfo.src) { + nodeContent.push({ label: 'Image Source', value: imageInfo.src }); + } + } + } + + const textBoxContent = this.getTextBoxContent(info.hoveredNode); + if (textBoxContent) { + nodeContent.push({ label: 'Text', value: textBoxContent }); + } + } + + const importantParameters = this.getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + + sections.push({ + id: 'node', + icon: '🎯', + title: 'Node', + badge: info.hoveredNode.type, + content: nodeContent + }); + } + + return sections; + } + + renderSections(sections) { + if (sections.length === 0) { + this.elements.content.innerHTML = ` +
+
📍
+
Empty canvas area
+
+ `; + return; + } + + this.elements.content.innerHTML = sections.map(section => ` +
+
+ ${section.icon} + ${section.title} + ${section.badge ? `${section.badge}` : ''} + ${section.id !== 'node' ? '▶' : ''} +
+
+
+ ${section.content.map(item => { + const value = this.formatValue(item.value, item.label); + const valueClass = this.getValueClass(item.value); + const valueAttributes = this.getValueAttributes(item.value); + return ` +
+ ${item.label} + ${value} +
`; + }).join('')} +
+
+
+ `).join(''); + } + + updateSectionStates() { + const sections = this.elements.content.querySelectorAll('.info-section'); + sections.forEach(section => { + const sectionId = section.dataset.section; + const header = section.querySelector('.section-header'); + const content = section.querySelector('.section-content'); + + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add('expanded'); + content.classList.add('expanded'); + } else { + header.classList.remove('expanded'); + content.classList.remove('expanded'); + } + }); + } + + formatValue(value, label) { + if (!value) return value; + + const str = String(value); + + // Show full text for prompts, text content, model names, and file paths + if (label && ( + label.toLowerCase().includes('text') || + label.toLowerCase().includes('prompt') || + label.toLowerCase().includes('model') || + label.toLowerCase().includes('file') || + label.toLowerCase().includes('conditioning') || + label.toLowerCase().includes('positive') || + label.toLowerCase().includes('negative') + )) { + return str; + } + + // Show full text for very long values (no truncation) + return str; + } + + getValueClass(value) { + if (!value) return ''; + + const str = String(value); + let classes = []; + + // Mark text that might benefit from special styling for readability + if (str.length > 100) { + classes.push('long-text'); + } + + return classes.join(' '); + } + + getValueAttributes(value) { + // Since we show full text now, we don't need title attributes for long text + // Only add title for very long text that might benefit from tooltips + if (!value) return ''; + + const str = String(value); + if (str.length > 500) { // Only for extremely long text + return `title="${str.replace(/"/g, '"')}"`; + } + + return ''; + } + + updateHeaderSubtitle(info) { + const subtitleElement = this.elements.header.querySelector('.header-subtitle'); + if (subtitleElement) { + const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = accentColor; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = accentColor; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = accentColor; + } else { + subtitleElement.textContent = 'Real-time analysis'; + subtitleElement.style.color = ''; + } + } + } + + // Helper methods for node information extraction + getCheckpointInfo(nodeInfo) { + if (nodeInfo.type && ( + nodeInfo.type.includes("CheckpointLoader") || + nodeInfo.type.includes("LoadCheckpoint") || + nodeInfo.type.includes("ModelLoader") || + nodeInfo.type.includes("UNETLoader") || + nodeInfo.type.includes("VAELoader") || + nodeInfo.type.includes("LoraLoader") + )) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("model") || + widget.name.toLowerCase().includes("checkpoint") || + widget.name.toLowerCase().includes("ckpt") || + widget.name.toLowerCase().includes("lora") || + widget.name.toLowerCase().includes("vae") || + widget.name.toLowerCase().includes("file") + )) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; + } + + getImageInfo(nodeInfo) { + if (nodeInfo.type && ( + nodeInfo.type.includes("SaveImage") || + nodeInfo.type.includes("PreviewImage") || + nodeInfo.type.includes("VisionOutput") || + nodeInfo.type.includes("ImageOutput") || + nodeInfo.type.includes("LoadImage") || + nodeInfo.type.includes("Display") + )) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("image") || + widget.name.toLowerCase().includes("filename") || + widget.name.toLowerCase().includes("file") + )) { + return widget.value; + } + } + } + + if (nodeInfo.properties && nodeInfo.properties.img) { + const img = nodeInfo.properties.img; + return { + width: img.width || "unknown", + height: img.height || "unknown", + src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" + }; + } + + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return "Image connected to " + output.links.length + " node(s)"; + } + } + } + + return "Image node"; + } + return null; + } + + getTextBoxContent(nodeInfo) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { + for (const widget of nodeInfo.widgets) { + if (widget.name === "text" && typeof widget.value === 'string') { + return widget.value; + } + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes("prompt") || + widget.name.toLowerCase().includes("conditioning")) && + typeof widget.value === 'string' && widget.value.length > 0) { + return widget.value; + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.type === 'text' || widget.type === 'textarea' || + widget.type === 'string' || widget.name.toLowerCase().includes('text')) && + typeof widget.value === 'string' && widget.value.length > 0) { + + // Show full text without truncation + return widget.value; + } + } + } + return null; + } + + getImportantNodeParameters(nodeInfo) { + const parameters = []; + + // For complex nodes that have many parameters, show ALL widgets + const nodeType = nodeInfo.type.toLowerCase(); + const isSaveNode = nodeType.includes('save') && + !nodeType.includes('checkpoint') && + !nodeType.includes('model') && + !nodeType.includes('preview'); + + const showAllWidgets = nodeInfo.type && ( + nodeType.includes('ksampler') || + nodeType.includes('sampler') || + nodeType.includes('k_samplers') || + nodeType.includes('checkpoint') || + nodeType.includes('model') || + nodeType.includes('lora') || + nodeType.includes('controlnet') || + nodeType.includes('advanced') || + nodeType.includes('detailer') || + nodeType.includes('inpaint') || + nodeType.includes('upscale') || + nodeType.includes('clip') || + nodeType.includes('text') || + nodeType.includes('encode') + ) && !isSaveNode; + + if (showAllWidgets) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + // Show all widgets for complex nodes, but filter out some duplicates + if (widget.name && widget.name !== '') { + const widgetName = widget.name.toLowerCase(); + + // Skip some generic widget names that might be duplicates + // These are handled by specific extraction methods for simple nodes + if (widgetName.includes('title') || + widgetName === 'node' || + widgetName === 'id' || + widgetName === 'type' || + widgetName === 'mode') { + continue; + } + + parameters.push({ + label: widget.name, + value: this.formatValue(widget.value) + }); + } + } + } + return parameters; + } + + // Special handling for Save nodes - only show essential parameters + let importantParams; + if (isSaveNode) { + importantParams = [ + 'filename_prefix', 'filename', 'directory', 'path', + 'format', 'quality', 'extension' + ]; + } else { + // For other nodes, use the filtered list but avoid duplicates with specific extractions + importantParams = [ + 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', + 'positive', 'negative', 'width', 'height', 'denoise', 'strength', + 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', + // KSampler specific parameters (for other samplers) + 'control_after_generate', 'control', 'after', 'generate', + 'start_at_step', 'end_at_step', 'start', 'end', + 'return_with_leftover_noise', 'leftover', 'noise_return', + // Additional common parameters (but avoid duplicates with specific extractors) + 'model', 'vae', 'clip', 'lora', 'checkpoint', + 'latent', 'image', 'mask', 'filename', 'directory', + 'prompt', 'conditioning', 'filename_prefix', + // New detection vocabulary + 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', + 'style_type', 'background', 'n', 'human', 'raw', 'guidance', + 'skip_preprocessing', 'movement_amplitude', 'animation', + 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', + 'sigma', 'rho', + // Additional detection vocabulary + 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', + 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', + 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', + 'set_cond_area', 'audioui', + // Camera and 3D parameters + 'camera_pose', 'fx', 'cx', 'fy', 'cy' + // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent + ]; + } + + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some(param => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: this.formatValue(widget.value) + }); + } + } + } + + return parameters; + } + + formatValue(value) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value; // Show full text without truncation + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + + injectStyles() { + if (!document.getElementById('magnify-info-panel-styles-v2')) { + const style = document.createElement('style'); + style.id = 'magnify-info-panel-styles-v2'; + style.textContent = this.getCSS(); + document.head.appendChild(style); + } + } + + getCSS() { + return ` + .magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.6; + pointer-events: auto; + } + + .magnify-info-panel * { + pointer-events: auto; + } + + .magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + color: var(--info-panel-text-color, #e0e0e0); + border: 1px solid rgba(100, 100, 120, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + } + + .magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + color: var(--info-panel-text-color, #2a2a2a); + border: 1px solid rgba(200, 200, 220, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); + } + + .magnify-info-panel.visible { + transform: translateY(0) !important; + } + + .magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; + } + + .magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 100001 !important; + transition: none !important; + } + + .magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; + } + + .panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; + } + + .magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .floating-controls { + position: fixed; + display: flex; + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 100000; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + } + + .floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; + } + + .floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; + } + + .control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; + } + + .control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; + } + + .control-btn:active { + background: rgba(100, 100, 110, 0.9); + } + + .control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; + } + + .control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; + } + + .control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; + } + + .panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + } + + .header-content { + display: flex; + align-items: center; + gap: 10px; + } + + .header-icon { + font-size: 18px; + opacity: 0.9; + } + + .header-title { + font-weight: 600; + font-size: 18px; + } + + .header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; + } + + .header-controls { + display: flex; + gap: 6px; + } + + .minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; + } + + .minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); + } + + .minimize-btn:active { + transform: scale(0.95); + } + + .panel-content { + padding: 0; + max-height: calc(100% - 60px); + overflow-y: auto; + scrollbar-width: thin; + } + + .panel-content::-webkit-scrollbar { + width: 8px; + } + + .panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + } + + .panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; + } + + .info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 8px; + border-radius: 10px; + overflow: visible; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; + } + + .info-section:hover { + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); + } + + .section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border-radius: 8px; + margin: 3px; + position: relative; + overflow: hidden; + } + + .section-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); + transform: translateX(-100%); + transition: transform 0.6s ease; + } + + .section-header:hover::before { + transform: translateX(100%); + } + + .section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); + } + + .section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); + } + + .section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); + } + + .info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; + } + + .info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; + } + + .info-section[data-section="node"] .section-title { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 700; + } + + .info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; + } + + .info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="media"] .section-title { + color: var(--info-panel-accent-color, #fdcb6e); + font-weight: 700; + } + + .section-icon { + font-size: 18px; + width: 22px; + text-align: center; + } + + .section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + .section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + } + + .expand-icon { + font-size: 14px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.6); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + margin-left: auto; + } + + .section-header:hover .expand-icon { + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.15); + transform: scale(1.1); + } + + .section-header.expanded .expand-icon { + transform: rotate(90deg) scale(1.1); + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.2); + } + + .section-content { + max-height: 0; + overflow: visible; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 0; + } + + .section-content.expanded { + max-height: 2000px; + opacity: 1; + overflow: visible; + } + + .section-body { + padding: 0 18px 16px 24px; + } + + .info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + border-radius: 4px; + margin: 0 -8px; + padding-left: 8px; + padding-right: 8px; + } + + .info-row:last-child { + border-bottom: none; + } + + .info-row:hover { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.08); + } + + .info-label { + font-weight: 500; + opacity: 0.85; + font-size: 15px; + min-width: 80px; + padding-right: 12px; + line-height: 1.5; + } + + .info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: var(--info-panel-accent-color, #a0d468); + background: rgba(160, 212, 104, 0.12); + padding: 4px 10px; + border-radius: 6px; + max-width: 100%; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + text-align: left; + white-space: pre-wrap; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + } + + .info-value:hover { + background: rgba(160, 212, 104, 0.18); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } + + .info-value.long-text { + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + line-height: 1.4; + } + + .empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; + } + + .empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; + } + + /* Copy to clipboard functionality */ + .info-value[title]:hover::after { + content: '📋'; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + opacity: 0.7; + } + + /* Smooth loading states */ + .section-body.loading { + opacity: 0.5; + pointer-events: none; + } + + .section-body.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); + } + + @keyframes spin { + to { transform: translate(-50%, -50%) rotate(360deg); } + } + + /* Enhanced scrollbar for better UX */ + .section-body::-webkit-scrollbar { + width: 4px; + } + + .section-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 2px; + } + + .section-body::-webkit-scrollbar-thumb { + background: var(--info-panel-accent-color, #74b9ff); + border-radius: 2px; + opacity: 0.6; + } + + .section-body::-webkit-scrollbar-thumb:hover { + opacity: 1; + } + + .empty-state-text { + font-size: 16px; + font-style: italic; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + .pulse { + animation: pulse 2s infinite; + } + `; + } + + cleanup() { + if (this.elements.panel) { + this.elements.panel.remove(); + } + + if (this.elements.controls) { + this.elements.controls.remove(); + } + + const styles = document.getElementById('magnify-info-panel-styles-v2'); + if (styles) { + styles.remove(); + } + } + } + + /** + * Position Manager + * Handles all positioning logic including pinning and boundary checking + */ + class PositionManager { + constructor(stateManager, uiManager) { + this.stateManager = stateManager; + this.uiManager = uiManager; + } + + positionPanel() { + if (!this.uiManager.elements.panel) return; + + // If panel is pinned, keep it at its pinned position + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + this.positionFloatingControls(); + return; + } + + // Normal positioning logic + this.calculateNormalPosition(); + this.positionFloatingControls(); + } + + positionFloatingControls() { + if (!this.uiManager.elements.controls) return; + + const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + + let idealX, idealY; + // Determine dimensions based on layout. Fallback values are estimates. + const isHorizontal = ['top', 'bottom'].includes(controlsPosition); + const controlsWidth = this.uiManager.elements.controls.offsetWidth || (isHorizontal ? 160 : 40); + const controlsHeight = this.uiManager.elements.controls.offsetHeight || (isHorizontal ? 40 : 160); + const margin = 5; + + // Determine the ideal, unconstrained position for the controls + switch (controlsPosition) { + case "top-left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.top; + break; + case "top-right": + idealX = panelRect.right + margin; + idealY = panelRect.top; + break; + case "bottom-left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.bottom - controlsHeight; + break; + case "bottom-right": + idealX = panelRect.right + margin; + idealY = panelRect.bottom - controlsHeight; + break; + case "top": + idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; + idealY = panelRect.top - controlsHeight - margin; + break; + case "bottom": + idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; + idealY = panelRect.bottom + margin; + break; + case "left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; + break; + case "right": + idealX = panelRect.right + margin; + idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; + break; + default: // Fallback to top-right + idealX = panelRect.right + margin; + idealY = panelRect.top; + break; + } + + let finalX = idealX; + let finalY = idealY; + + // For side-positioned controls, check if they are forced to move horizontally. + // If so, override their vertical alignment to be "top" instead of "center". + if (['left', 'right'].includes(controlsPosition)) { + if (idealX < margin || (idealX + controlsWidth) > (window.innerWidth - margin)) { + finalY = panelRect.top; // Switch to top alignment + + // Also, place it slightly inside the panel to ensure it's visible. + if (controlsPosition === 'left') { + finalX = margin; + } else { // 'right' + finalX = window.innerWidth - controlsWidth - margin; + } + } + } + + // Apply boundary constraints to the final calculated position + finalX = Math.max(margin, Math.min(finalX, window.innerWidth - controlsWidth - margin)); + finalY = Math.max(margin, Math.min(finalY, window.innerHeight - controlsHeight - margin)); + + this.uiManager.elements.controls.style.left = `${finalX}px`; + this.uiManager.elements.controls.style.top = `${finalY}px`; + + // Update layout class based on the setting + this.uiManager.updateControlsLayout(controlsPosition); + } + + applyPinnedPosition() { + let { x, y } = this.stateManager.state.pinnedPosition; + const panel = this.uiManager.elements.panel; + const panelWidth = panel.offsetWidth; + const panelHeight = panel.offsetHeight; + const margin = 10; + + // Ensure the pinned panel stays within the viewport boundaries, e.g., after content expands. + // This adjustment is now temporary and won't update the stored pinned position. + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + + panel.style.left = `${boundedX}px`; + panel.style.top = `${boundedY}px`; + } + + calculateNormalPosition() { + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], this.uiManager.elements.panel.scrollHeight); + + let left, top; + const margin = 15; + + if (!this.stateManager.state.isGlassPreviewVisible) { + // Position at mouse cursor when glass is hidden + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } else { + // Position relative to magnify glass + const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); + if (glassRect) { + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + + switch (position) { + case "Right": + left = glassRect.right + margin; + top = glassRect.top; + break; + case "Left": + left = glassRect.left - panelWidth - margin; + top = glassRect.top; + break; + case "Top": + left = glassRect.left; + top = glassRect.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect.left; + top = glassRect.bottom + margin; + break; + default: + left = glassRect.right + margin; + top = glassRect.top; + break; + } + } else { + // Fallback to mouse position + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } + } + + // Apply boundary constraints + left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); + + this.uiManager.elements.panel.style.left = `${left}px`; + this.uiManager.elements.panel.style.top = `${top}px`; + } + } + + /** + * Event Manager + * Handles all event binding and delegation + */ + class EventManager { + constructor(stateManager, uiManager, positionManager) { + this.stateManager = stateManager; + this.uiManager = uiManager; + this.positionManager = positionManager; + + this.setupEventListeners(); + } + + setupEventListeners() { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + + setupPanelEvents() { + // Event delegation for panel controls + this.uiManager.elements.panel.addEventListener('click', (e) => { + const action = e.target.dataset.action; + if (action) { + e.stopPropagation(); + e.preventDefault(); + this.handleControlAction(action); + } + + // Section header clicks + const sectionHeader = e.target.closest('.section-header'); + if (sectionHeader && sectionHeader.dataset.section) { + const sectionId = sectionHeader.dataset.section; + if (sectionId !== 'node') { // Node section is always expanded + this.stateManager.toggleSection(sectionId); + this.uiManager.updateSectionStates(); + } + } + }); + + // Event delegation for floating controls (separate since they're not children of panel) + if (this.uiManager.elements.controls) { + this.uiManager.elements.controls.addEventListener('click', (e) => { + const action = e.target.dataset.action; + if (action) { + e.stopPropagation(); + e.preventDefault(); + this.handleControlAction(action); + } + }); + + // Prevent floating controls from propagating events + this.uiManager.elements.controls.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + + this.uiManager.elements.controls.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); + } + + // Prevent panel interactions from propagating + this.uiManager.elements.panel.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + + this.uiManager.elements.panel.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); + } + + // Helper function to check if user is typing in an input field + isUserTyping() { + const activeElement = document.activeElement; + if (!activeElement) return false; + + // Check if the active element is an input, textarea, or contenteditable + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea') { + return true; + } + + // Check for contenteditable elements + if (activeElement.contentEditable === 'true') { + return true; + } + + // Check if it's inside a form or has input-like classes + if (activeElement.closest('form') || + activeElement.classList.contains('cm-editor') || // CodeMirror editor + activeElement.classList.contains('monaco-editor') || // Monaco editor + activeElement.closest('.cm-editor') || + activeElement.closest('.monaco-editor')) { + return true; + } + + return false; + } + + setupHotkeyEvents() { + document.addEventListener('keydown', (e) => { + // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) + if (this.isUserTyping()) { + return; + } + + // Only handle hotkeys when magnifying glass is active + if (!magnifyGlass.state.active) return; + + const settings = this.stateManager.state.settings; + + // Toggle info panel + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && + (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('toggle-panel'); + } + + // Toggle glass preview + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && + (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('toggle-glass'); + } + + // Unlock panel from magnify glass and move to current mouse location (no Alt required due to Smart Input Detection) + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('pin-at-mouse'); + } + }); + } + + setupDragEvents() { + let dragState = { + isDragging: false, + startX: 0, + startY: 0, + currentX: 0, + currentY: 0, + offsetX: 0, + offsetY: 0 + }; + + // Start drag + const startDrag = (e) => { + // Only when pinned and not locked + if (!this.stateManager.state.isPanelPinned || this.stateManager.state.isPanelLocked) return; + + // Block on specific elements only + if (e.target.closest('.minimize-btn') || + e.target.closest('.control-btn') || + e.target.tagName === 'BUTTON') { + return; + } + + // console.log('[DRAG] Starting drag'); + + // Stop all event propagation immediately + e.preventDefault(); + e.stopImmediatePropagation(); + + // Set drag state + dragState.isDragging = true; + dragState.startX = e.clientX; + dragState.startY = e.clientY; + + // Get current panel position + const rect = this.uiManager.elements.panel.getBoundingClientRect(); + dragState.currentX = rect.left; + dragState.currentY = rect.top; + dragState.offsetX = e.clientX - rect.left; + dragState.offsetY = e.clientY - rect.top; + + // Visual feedback + this.uiManager.elements.panel.classList.add('panel-dragging'); + this.uiManager.elements.panel.style.cursor = 'grabbing'; + document.body.style.cursor = 'grabbing'; + document.body.style.userSelect = 'none'; + + // console.log('[DRAG] Drag started at:', { x: dragState.currentX, y: dragState.currentY }); + }; + + // Handle drag movement + const doDrag = (e) => { + if (!dragState.isDragging) return; + + e.preventDefault(); + e.stopImmediatePropagation(); + + // Calculate new position based on mouse movement + const newX = e.clientX - dragState.offsetX; + const newY = e.clientY - dragState.offsetY; + + // Apply boundaries + const panelWidth = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = this.uiManager.elements.panel.offsetHeight; + const margin = 10; + + const boundedX = Math.max(margin, Math.min(newX, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(newY, window.innerHeight - panelHeight - margin)); + + // Apply position immediately and forcefully + this.uiManager.elements.panel.style.position = 'fixed'; + this.uiManager.elements.panel.style.left = `${boundedX}px`; + this.uiManager.elements.panel.style.top = `${boundedY}px`; + this.uiManager.elements.panel.style.transform = 'none'; + this.uiManager.elements.panel.style.zIndex = '10010'; + + // Update current position + dragState.currentX = boundedX; + dragState.currentY = boundedY; + + // Update floating controls + if (this.uiManager.elements.controls) { + this.positionManager.positionFloatingControls(); + } + + // console.log('[DRAG] Moving to:', { x: boundedX, y: boundedY }); + }; + + // End drag + const endDrag = (e) => { + if (!dragState.isDragging) return; + + // console.log('[DRAG] Ending drag'); + + e.preventDefault(); + e.stopImmediatePropagation(); + + // Calculate if we actually moved significantly + const deltaX = Math.abs(e.clientX - dragState.startX); + const deltaY = Math.abs(e.clientY - dragState.startY); + const moved = deltaX > 5 || deltaY > 5; + + if (moved) { + // Save the final position + this.stateManager.setPinnedPosition(dragState.currentX, dragState.currentY); + // console.log('[DRAG] Position saved:', { x: dragState.currentX, y: dragState.currentY }); + } + + // Reset state + dragState.isDragging = false; + + // Remove visual feedback + this.uiManager.elements.panel.classList.remove('panel-dragging'); + this.uiManager.elements.panel.style.cursor = ''; + this.uiManager.elements.panel.style.zIndex = ''; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + // Cancel drag + const cancelDrag = () => { + if (!dragState.isDragging) return; + + // console.log('[DRAG] Cancelling drag'); + + // Restore original position from pinned state + const { x, y } = this.stateManager.state.pinnedPosition; + this.uiManager.elements.panel.style.left = `${x}px`; + this.uiManager.elements.panel.style.top = `${y}px`; + + // Reset state + dragState.isDragging = false; + + // Remove visual feedback + this.uiManager.elements.panel.classList.remove('panel-dragging'); + this.uiManager.elements.panel.style.cursor = ''; + this.uiManager.elements.panel.style.zIndex = ''; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Update controls + if (this.uiManager.elements.controls) { + this.positionManager.positionFloatingControls(); + } + }; + + // Attach events with capture to prevent interference + this.uiManager.elements.panel.addEventListener('mousedown', startDrag, { capture: true, passive: false }); + document.addEventListener('mousemove', doDrag, { capture: true, passive: false }); + document.addEventListener('mouseup', endDrag, { capture: true, passive: false }); + + // Handle escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + cancelDrag(); + } + }); + + // Handle mouse leaving window + document.addEventListener('mouseleave', endDrag); + + // Visual feedback + this.uiManager.elements.panel.addEventListener('mouseenter', () => { + if (this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked && !dragState.isDragging) { + this.uiManager.elements.panel.style.cursor = 'grab'; + } + }); + + this.uiManager.elements.panel.addEventListener('mouseleave', () => { + if (!dragState.isDragging) { + this.uiManager.elements.panel.style.cursor = ''; + } + }); + + // console.log('[DRAG] Event listeners attached with capture mode'); + + // Store cleanup function + this.dragCleanup = () => { + this.uiManager.elements.panel.removeEventListener('mousedown', startDrag, { capture: true }); + document.removeEventListener('mousemove', doDrag, { capture: true }); + document.removeEventListener('mouseup', endDrag, { capture: true }); + document.removeEventListener('mouseleave', endDrag); + cancelDrag(); + }; + } + + setupHoverEvents() { + this.uiManager.elements.panel.addEventListener('mouseenter', () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + + this.uiManager.elements.panel.addEventListener('mouseleave', () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + + captureCurrentPanelPosition() { + // Capture current position when pinning is activated + const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); + this.stateManager.setPinnedPosition(panelRect.left, panelRect.top); + // console.log(`Panel pinned at current position: (${panelRect.left}, ${panelRect.top})`); + } + + handleControlAction(action) { + switch (action) { + case 'minimize': + this.stateManager.toggleMinimized(); + this.uiManager.updateMinimizedState(); + break; + + case 'pin': + // If not currently pinned, either restore to last position or capture current + if (!this.stateManager.state.isPanelPinned) { + // If we don't have a remembered position, capture current position + if (!this.stateManager.state.lastPinnedPosition) { + this.captureCurrentPanelPosition(); + } + // Note: togglePinning will restore lastPinnedPosition if it exists + } + + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + this.positionManager.positionPanel(); + break; + + case 'lock': + this.stateManager.toggleLocking(); + this.uiManager.updatePinnedState(); // This updates both pinned and locked states + break; + + case 'pin-at-mouse': + // Pin at current mouse location (Alt + configured key) + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], 400); + + // Position panel near mouse with some offset to avoid covering cursor area + const mouseX = magnifyGlass.lastKnownMousePosition.x; + const mouseY = magnifyGlass.lastKnownMousePosition.y; + const pinKey = settings["🔍MagnifyGlass.PinPanelHotkey"]; + + let pinX = mouseX + 50; // Offset to the right of mouse + let pinY = mouseY - 100; // Offset above mouse + + // Apply boundary constraints + pinX = Math.max(10, Math.min(pinX, window.innerWidth - panelWidth - 10)); + pinY = Math.max(10, Math.min(pinY, window.innerHeight - panelHeight - 10)); + + // Set the new pin position (this will also save as lastPinnedPosition) + this.stateManager.setPinnedPosition(pinX, pinY); + + // Enable pinning if not already pinned + if (!this.stateManager.state.isPanelPinned) { + this.stateManager.state.isPanelPinned = true; + } + + this.uiManager.updatePinnedState(); + this.positionManager.positionPanel(); + // console.log(`Panel pinned at mouse location: (${pinX}, ${pinY}) using Alt+${pinKey.toUpperCase()}`); + break; + + case 'toggle-panel': + if (this.stateManager.togglePanelVisibility()) { + this.uiManager.show(); + } else { + this.uiManager.hide(); + } + this.uiManager.updateControlStates(); + break; + + case 'toggle-glass': + const isVisible = this.stateManager.toggleGlassPreview(); + this.applyGlassVisibility(isVisible); + + // When the glass preview is hidden, automatically pin the panel at its current location. + if (!isVisible && !this.stateManager.state.isPanelPinned) { + this.captureCurrentPanelPosition(); // Capture current position before pinning + this.stateManager.togglePinning(); + this.stateManager.state.isAutoPinned = true; // Mark as auto-pinned + this.uiManager.updatePinnedState(); + } + + // When the glass preview is shown again, only auto-unlock if NOT manually locked with 📌 button + if (isVisible && this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked) { + this.stateManager.togglePinning(); // Unpin the panel + this.stateManager.state.isAutoPinned = false; // Clear auto-pin flag + this.uiManager.updatePinnedState(); + } + + this.uiManager.updateControlStates(); + this.positionManager.positionPanel(); // Reposition based on new glass state + break; + } + } + + applyGlassVisibility(isVisible) { + if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { + magnifyGlass.ui.glassDiv.style.opacity = isVisible ? "1" : "0"; + } + } + } + + /** + * Information Gatherer + * Responsible for collecting information about the current state + */ + class InformationGatherer { + constructor() {} + + gatherInformation() { + const info = { + timestamp: Date.now(), + cursor: { + canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, + screen: { + x: magnifyGlass.lastKnownMousePosition.x, + y: magnifyGlass.lastKnownMousePosition.y + } + }, + canvas: { + scale: magnifyGlass.state.canvasScale, + offset: { + x: magnifyGlass.state.canvasOffsetX, + y: magnifyGlass.state.canvasOffsetY + } + }, + magnifier: { + zoomFactor: magnifyGlass.config.zoomFactor, + offsetX: magnifyGlass.config.offsetX, + offsetY: magnifyGlass.config.offsetY, + sourceRegion: { + x: magnifyGlass.state.sourceX, + y: magnifyGlass.state.sourceY, + width: magnifyGlass.state.sourceWidth, + height: magnifyGlass.state.sourceHeight + } + }, + hoveredNode: null, + node: null, + widget: null, + connection: null, + media: null + }; + + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + info.node = this.getNodeInfo(nodeUnderCursor.node); + + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.widget = this.getWidgetInfo(widget); + } + } + + return info; + } + + getNodeUnderCursor() { + if (!app.graph || !app.canvas) { + return null; + } + + try { + // Try to get the node from ComfyUI's state + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + + // Fallback to manual detection + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || node.flags?.collapsed) continue; + + if (graphPos[0] >= node.pos[0] && + graphPos[0] <= node.pos[0] + node.size[0] && + graphPos[1] >= node.pos[1] && + graphPos[1] <= node.pos[1] + node.size[1]) { + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + } catch (err) { + console.warn("Error in node detection:", err); + } + + return null; + } + + getDetailedNodeInfo(node, localPos) { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: ((localPos.x / node.size[0]) * 100).toFixed(1), + y: ((localPos.y / node.size[1]) * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + + getNodeInfo(node) { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + + getWidgetUnderCursor(node, localPos) { + if (!node.widgets || !node.widgets.length) return null; + + const titleHeight = 30; + let currentY = titleHeight; + + for (const widget of node.widgets) { + const widgetHeight = 25; + + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + + currentY += widgetHeight + 5; + } + + return null; + } + + getWidgetInfo(widget) { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + + getMediaInfo(mediaElement) { + const info = { + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" + }; + + if (mediaElement.tagName === 'IMG') { + info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; + info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; + info.complete = mediaElement.complete; + } else if (mediaElement.tagName === 'VIDEO') { + info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; + info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; + info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; + info.paused = mediaElement.paused; + info.readyState = mediaElement.readyState; + } + + return info; + } + + getNodeModeText(mode) { + const modes = { + 0: "Always", + 1: "On Event", + 2: "Never", + 3: "On Trigger", + 4: "On Request" + }; + return modes[mode] || `Mode ${mode}`; + } + + detectNodeRegion(localPos, node) { + const titleHeight = 30; + const regions = []; + + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); + } + + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); + } + + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); + } + + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } + } + + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); + } + + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + + formatValue(value) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value; // Show full text without truncation + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + } + + /** + * Main Professional Info Panel Manager + * Orchestrates all the other components + */ + class ProfessionalInfoPanelManager { + constructor(magnifyGlass) { + this.magnifyGlass = magnifyGlass; + + // Initialize all managers + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.positionManager = new PositionManager(this.stateManager, this.uiManager); + this.eventManager = new EventManager(this.stateManager, this.uiManager, this.positionManager); + this.informationGatherer = new InformationGatherer(); + + this.hookIntoMagnifyGlass(); + + console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); + } + + hookIntoMagnifyGlass() { + // Hook into magnify glass update cycle + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + + // Hook into show/hide + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.uiManager.show(); + // Position controls after showing + setTimeout(() => this.positionManager.positionPanel(), 10); + } + }).bind(this); + + this.magnifyGlass.ui.hide = (() => { + originalHide(); + this.uiManager.hide(); + }).bind(this); + } + + scheduleInfoUpdate() { + if (this.stateManager.state.updateScheduled) return; + + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + + updateInfo() { + if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; + + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + this.uiManager.displayInfo(info); + this.positionManager.positionPanel(); + + // Handle node hover state + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); + } + } + } + } + + updateSettings() { + const changes = this.stateManager.updateSettings(); + + // React to setting changes (theme is now auto-detected) + // Theme changes are handled automatically by the observer + + if (changes["🔍MagnifyGlass.ControlsPosition"]) { + // Update controls layout and position when position setting changes + this.positionManager.positionPanel(); + } + + // Apply new styles + this.uiManager.applyStyles(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + } else { + this.uiManager.hide(); + } + } + + cleanup() { + this.stateManager.cleanup(); + this.uiManager.cleanup(); + } + } + + // Create the main manager instance + const infoPanelManager = new ProfessionalInfoPanelManager(magnifyGlass); + + // Store reference globally for cleanup and theme updates + if (!window.comfyUIMagnifyGlassExtensions) { + window.comfyUIMagnifyGlassExtensions = []; + } + window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); + + // Store main manager reference for theme updates + window.infoPanelManager = infoPanelManager; + + // Register settings with ComfyUI + Object.keys(DEFAULT_SETTINGS).forEach(settingKey => { + const settingConfig = getSettingConfig(settingKey, DEFAULT_SETTINGS[settingKey]); + if (settingConfig) { + app.ui.settings.addSetting({ + ...settingConfig, + onChange: (value) => { + infoPanelManager.updateSettings(); + } + }); + } + }); + + function getSettingConfig(key, defaultValue) { + const configs = { + "🔍MagnifyGlass.InfoPanelEnabled": { + id: key, + name: "📋 Magnify Glass: Info Panel", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable the professional information panel." + }, + "🔍MagnifyGlass.InfoPanelPosition": { + id: key, + name: "📋 Magnify Glass: Info Panel Position", + type: "combo", + options: [ + { value: "Right", text: "Right" }, + { value: "Left", text: "Left" }, + { value: "Top", text: "Top" }, + { value: "Bottom", text: "Bottom" } + ], + defaultValue, + tooltip: "Position of the info panel relative to the magnifying glass." + }, + "🔍MagnifyGlass.InfoPanelWidth": { + id: key, + name: "📋 Magnify Glass: Info Panel Width", + type: "slider", + defaultValue, + min: 250, + max: 450, + step: 10, + tooltip: "Width of the information panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelOpacity": { + id: key, + name: "📋 Magnify Glass: Info Panel Opacity (%)", + type: "slider", + defaultValue, + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the information panel background as a percentage (10 = very transparent, 100 = fully opaque).", + onChange: (value) => { + // Convert percentage to decimal and apply opacity immediately + const opacity = value / 100; + if (window.infoPanelManager && window.infoPanelManager.uiManager && window.infoPanelManager.uiManager.elements.panel) { + window.infoPanelManager.uiManager.elements.panel.style.opacity = opacity; + } + } + }, + "🔍MagnifyGlass.InfoPanelMaxHeight": { + id: key, + name: "📋 Magnify Glass: Info Panel Max Height", + type: "slider", + defaultValue, + min: 300, + max: 700, + step: 25, + tooltip: "Maximum height of the information panel in pixels." + }, + + "🔍MagnifyGlass.InfoPanelAnimations": { + id: key, + name: "🎬 Magnify Glass: Info Panel Animations", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable animations for the info panel." + }, + "🔍MagnifyGlass.ShowInspectorTab": { + id: key, + name: "📋 Magnify Glass: Show Inspector Tab", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Show or hide the Inspector tab with cursor and canvas information." + }, + "🔍MagnifyGlass.ToggleHotkey": { + id: key, + name: "🔑 Magnify Glass: Info Panel Toggle Hotkey", + type: "combo", + options: ["i", "p", "o", "l", "k"], + defaultValue, + tooltip: "Key to toggle the info panel visibility while the magnifier is active." + }, + "🔍MagnifyGlass.GlassPreviewToggleHotkey": { + id: key, + name: "🔑 Magnify Glass: Preview Toggle Hotkey", + type: "combo", + options: ["g", "v", "b", "m", "n"], + defaultValue, + tooltip: "Key to toggle the magnifying glass preview visibility." + }, + "🔍MagnifyGlass.PinPanelHotkey": { + id: key, + name: "🔓 Magnify Glass: Unlock Panel to Mouse Location", + type: "combo", + options: ["u", "p", "f", "x", "z"], + defaultValue, + tooltip: "Key to unlock the info panel from the magnify glass and move it to your current mouse position (e.g., U). Panel remains movable - use the pin button to lock it in place. Alt not required due to Smart Input Detection." + }, + "🔍MagnifyGlass.ShowHoveringControls": { + id: key, + name: "🎮 Magnify Glass: Show Hover Controls", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Show or hide hovering UI controls above the info panel." + }, + "🔍MagnifyGlass.ControlsPosition": { + id: key, + name: "📍 Magnify Glass: Controls Position", + type: "combo", + options: [ + { value: "top-left", text: "Top-Left" }, + { value: "top-right", text: "Top-Right" }, + { value: "bottom-left", text: "Bottom-Left" }, + { value: "bottom-right", text: "Bottom-Right" }, + { value: "top", text: "Top (Centered)" }, + { value: "bottom", text: "Bottom (Centered)" }, + { value: "left", text: "Left (Centered)" }, + { value: "right", text: "Right (Centered)" } + ], + defaultValue, + tooltip: "Position of the floating control buttons relative to the info panel." + }, + "🔍MagnifyGlass.InfoPanelTextColor": { + id: key, + name: "🎨 Magnify Glass: Info Panel Text Color", + type: "color", + defaultValue, + tooltip: "Color of the text in the info panel. This affects all text content including titles, values, and labels.", + onChange: (value) => { + // Ensure color value includes # symbol for valid CSS and persistence + const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; + + // Only save back if we actually normalized the color (to prevent recursion) + if (value !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized text color:', e); + } + } + + // Apply the color immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.applyStyles(); + } + } + }, + "🔍MagnifyGlass.InfoPanelAccentColor": { + id: key, + name: "🎨 Magnify Glass: Info Panel Accent Color", + type: "color", + defaultValue, + tooltip: "Color used for highlighted elements like values, section titles, and active states in the info panel.", + onChange: (value) => { + // Ensure color value includes # symbol for valid CSS and persistence + const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; + + // Only save back if we actually normalized the color (to prevent recursion) + if (value !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized accent color:', e); + } + } + + // Apply the color immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.applyStyles(); + } + } + } + }; + + return configs[key] || null; + } + + console.log("ComfyUI Magnify Info Panel Pro V2: All settings registered and initialized"); + } + } +}); \ No newline at end of file diff --git a/web/magnify_info_panel_refactored.js b/web/magnify_info_panel_refactored.js deleted file mode 100644 index dea1ef4..0000000 --- a/web/magnify_info_panel_refactored.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * ComfyUI MagnifyGlass - Info Panel (Refactored Entry Point) - * - * This is the refactored version of the info panel that uses modular imports. - * The original magnify_info_panel.js is preserved as a backup. - */ - -import { app } from "../../../scripts/app.js"; -import { InfoPanel } from './info-panel/index.js'; -import { DEFAULT_PANEL_SETTINGS, getSettingConfig } from './shared/settings.js'; - -app.registerExtension({ - name: "Comfy.MagnifyGlass.InfoPanel.Refactored", - - async setup() { - // Wait for magnify glass to be ready - const checkForMagnifyGlass = () => { - return new Promise((resolve) => { - const check = () => { - if (window.comfyUIMagnifyGlass) { - resolve(window.comfyUIMagnifyGlass); - } else { - setTimeout(check, 100); - } - }; - check(); - }); - }; - - // Get magnify glass instance - const magnifyGlass = await checkForMagnifyGlass(); - - // Create info panel manager - const infoPanelManager = new InfoPanel(magnifyGlass); - - // Store reference globally for cleanup and theme updates - if (!window.comfyUIMagnifyGlassExtensions) { - window.comfyUIMagnifyGlassExtensions = []; - } - window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); - - // Store main manager reference for theme updates - window.infoPanelManager = infoPanelManager; - - // Register settings with ComfyUI - Object.keys(DEFAULT_PANEL_SETTINGS).forEach(settingKey => { - const settingConfig = getSettingConfig(settingKey, DEFAULT_PANEL_SETTINGS[settingKey]); - if (settingConfig) { - app.ui.settings.addSetting({ - ...settingConfig, - onChange: (value) => { - infoPanelManager.updateSettings(); - } - }); - } - }); - - console.log("ComfyUI Magnify Info Panel (Refactored): Loaded successfully"); - } -}); diff --git a/web/shared/settings.js b/web/shared/settings.js index 040b567..1e9b9de 100644 --- a/web/shared/settings.js +++ b/web/shared/settings.js @@ -513,3 +513,146 @@ export function registerPanelSettings(stateManager, uiManager, positionManager) } }); } + +/** + * Get a setting configuration object for ComfyUI settings registration. + * This function generates the setting config based on the key and default value. + * + * @param {string} key - The setting key + * @param {*} defaultValue - The default value for the setting + * @returns {object|null} - The setting configuration or null if not found + */ +export function getSettingConfig(key, defaultValue) { + const configs = { + "🔍MagnifyGlass.InfoPanelEnabled": { + id: key, + name: "📋 Magnify Glass: Info Panel", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable the professional information panel." + }, + "🔍MagnifyGlass.InfoPanelPosition": { + id: key, + name: "📋 Info Panel: Position", + type: "combo", + options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })), + defaultValue, + tooltip: "Position of the info panel relative to the magnify glass." + }, + "🔍MagnifyGlass.InfoPanelWidth": { + id: key, + name: "📋 Info Panel: Width (px)", + type: "slider", + defaultValue, + min: 200, + max: 600, + step: 20, + tooltip: "Width of the info panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelMaxHeight": { + id: key, + name: "📋 Info Panel: Max Height (px)", + type: "slider", + defaultValue, + min: 200, + max: 1500, + step: 50, + tooltip: "Maximum height of the info panel." + }, + "🔍MagnifyGlass.InfoPanelOpacity": { + id: key, + name: "📋 Info Panel: Opacity (%)", + type: "slider", + defaultValue, + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the info panel background." + }, + "🔍MagnifyGlass.InfoPanelAnimations": { + id: key, + name: "📋 Info Panel: Animations", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable smooth animations for the info panel." + }, + "🔍MagnifyGlass.ShowInspectorTab": { + id: key, + name: "📋 Info Panel: Show Inspector", + type: "combo", + options: [ + { value: true, text: "Yes" }, + { value: false, text: "No" } + ], + defaultValue, + tooltip: "Show the inspector tab with cursor and canvas information." + }, + "🔍MagnifyGlass.ToggleHotkey": { + id: key, + name: "📋 Info Panel: Toggle Hotkey", + type: "combo", + options: ["i", "j", "k", "l", "n", "m"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the info panel visibility." + }, + "🔍MagnifyGlass.GlassPreviewToggleHotkey": { + id: key, + name: "📋 Info Panel: Glass Toggle Hotkey", + type: "combo", + options: ["g", "f", "v", "b"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the magnify glass preview visibility." + }, + "🔍MagnifyGlass.PinPanelHotkey": { + id: key, + name: "📋 Info Panel: Pin Hotkey", + type: "combo", + options: ["u", "p", "y", "t"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to pin/unlock the info panel at the current mouse location." + }, + "🔍MagnifyGlass.ShowHoveringControls": { + id: key, + name: "📋 Info Panel: Floating Controls", + type: "combo", + options: [ + { value: true, text: "Show" }, + { value: false, text: "Hide" } + ], + defaultValue, + tooltip: "Show floating control buttons near the info panel." + }, + "🔍MagnifyGlass.ControlsPosition": { + id: key, + name: "📋 Info Panel: Controls Position", + type: "combo", + options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"].map(p => ({ value: p, text: p })), + defaultValue, + tooltip: "Position of the floating control buttons." + }, + "🔍MagnifyGlass.InfoPanelTextColor": { + id: key, + name: "🎨 Info Panel: Text Color", + type: "color", + defaultValue, + tooltip: "Color for text in the info panel." + }, + "🔍MagnifyGlass.InfoPanelAccentColor": { + id: key, + name: "🎨 Info Panel: Accent Color", + type: "color", + defaultValue, + tooltip: "Accent color for highlights in the info panel." + } + }; + + return configs[key] || null; +}