From e2965f7a38eb406e1ebd7e71466d3aae7a159b30 Mon Sep 17 00:00:00 2001 From: AEmotionStudio Date: Sat, 3 Jan 2026 04:03:51 -0800 Subject: [PATCH] feat: Extend theme detection to include dracula, gruvbox, and monokai, and synchronize themes with the popout viewer. --- src/info-panel/StateManager.ts | 12 +++ src/magnify-glass/PopOutManager.ts | 36 ++++++- web/info-panel/StateManager.js | 10 ++ web/info-panel/StateManager.js.map | 2 +- web/magnify-glass/PopOutManager.js | 25 ++++- web/magnify-glass/PopOutManager.js.map | 2 +- web/popout-viewer.html | 127 +++++++++++++++++++++++++ 7 files changed, 208 insertions(+), 6 deletions(-) diff --git a/src/info-panel/StateManager.ts b/src/info-panel/StateManager.ts index ab563b2..9ce2a48 100644 --- a/src/info-panel/StateManager.ts +++ b/src/info-panel/StateManager.ts @@ -180,6 +180,12 @@ export class StateManager { detectedTheme = 'github'; } else if (classes.includes('theme-light') || classes.includes('light-theme')) { detectedTheme = 'light'; + } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) { + detectedTheme = 'dracula'; + } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) { + detectedTheme = 'gruvbox'; + } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) { + detectedTheme = 'monokai'; } } @@ -279,6 +285,12 @@ export class StateManager { if (window.infoPanelManager && window.infoPanelManager.uiManager) { window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); } + + // Notify Pop-Out Manager (if it exists on the main glass instance) + const magnifyGlass = (window as any).comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.popOutManager) { + magnifyGlass.popOutManager.updateTheme(this.state.currentTheme); + } } loadSettings(): void { diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts index 838dcd6..2fc30be 100644 --- a/src/magnify-glass/PopOutManager.ts +++ b/src/magnify-glass/PopOutManager.ts @@ -13,7 +13,7 @@ import { Logger } from '../shared/logger'; */ interface PopOutMessage { type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong'; - data?: string | PopOutConfig | PopOutInfo; + data?: string | Partial | PopOutInfo; timestamp?: number; } @@ -22,6 +22,7 @@ interface PopOutConfig { borderColor: string; borderWidth: number; glassShape: string; + theme?: string; } /** @@ -59,6 +60,7 @@ export class PopOutManager { private lastPongTime: number = 0; private pingInterval: number | null = null; private viewerUrl: string; + private currentTheme: string = 'dark'; // Default theme /** Throttle frame sending to ~30fps */ private lastFrameTime: number = 0; @@ -138,6 +140,8 @@ export class PopOutManager { if (!this.isOpen) { this.isOpen = true; Logger.debug('[PopOut] Viewer tab connected'); + // Send current theme immediately upon connection + this.sendConfig({ theme: this.currentTheme }); } break; case 'close': @@ -230,15 +234,41 @@ export class PopOutManager { /** * Send configuration to the pop-out viewer. */ - sendConfig(config: PopOutConfig): void { + /** + * Send configuration to the pop-out viewer. + */ + sendConfig(config: Partial): void { if (!this.channel) return; + // Merge with current theme if not provided + const finalConfig = { + theme: this.currentTheme, + ...config + }; + + // Update local theme cache if provided in config + if (config.theme) { + this.currentTheme = config.theme; + } + this.sendMessage({ type: 'config', - data: config + data: finalConfig }); } + /** + * Update the viewer theme. + * @param theme - Theme name (e.g. 'dark', 'light') + */ + updateTheme(theme: string): void { + if (this.currentTheme === theme) return; + + this.currentTheme = theme; + this.sendConfig({ theme }); + Logger.debug(`[PopOut] Theme updated to: ${theme}`); + } + /** * Send inspector info to the pop-out viewer. * @param info - Inspector panel information diff --git a/web/info-panel/StateManager.js b/web/info-panel/StateManager.js index 5ed2d6c..14e0222 100644 --- a/web/info-panel/StateManager.js +++ b/web/info-panel/StateManager.js @@ -103,6 +103,12 @@ class StateManager { detectedTheme = "github"; } else if (classes.includes("theme-light") || classes.includes("light-theme")) { detectedTheme = "light"; + } else if (classes.includes("theme-dracula") || classes.includes("dracula-theme")) { + detectedTheme = "dracula"; + } else if (classes.includes("theme-gruvbox") || classes.includes("gruvbox-theme")) { + detectedTheme = "gruvbox"; + } else if (classes.includes("theme-monokai") || classes.includes("monokai-theme")) { + detectedTheme = "monokai"; } } if (detectedTheme === "dark") { @@ -172,6 +178,10 @@ class StateManager { if (window.infoPanelManager && window.infoPanelManager.uiManager) { window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); } + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.popOutManager) { + magnifyGlass.popOutManager.updateTheme(this.state.currentTheme); + } } loadSettings() { Object.keys(DEFAULT_PANEL_SETTINGS).forEach((key) => { diff --git a/web/info-panel/StateManager.js.map b/web/info-panel/StateManager.js.map index 89d1b74..5ea65fe 100644 --- a/web/info-panel/StateManager.js.map +++ b/web/info-panel/StateManager.js.map @@ -1 +1 @@ -{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: GatheredInfo | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n wasPanelVisibleBeforeHide: false,\n isPanelMinimized: false,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: null,\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) {\n detectedTheme = 'light';\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n (typeof target.className === 'string' && target.className.toLowerCase().includes('theme'));\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]);\n }\n });\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: GatheredInfo | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n wasPanelVisibleBeforeHide: false,\n isPanelMinimized: false,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: null,\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) {\n detectedTheme = 'dracula';\n } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) {\n detectedTheme = 'gruvbox';\n } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) {\n detectedTheme = 'monokai';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) {\n detectedTheme = 'light';\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n (typeof target.className === 'string' && target.className.toLowerCase().includes('theme'));\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n\n // Notify Pop-Out Manager (if it exists on the main glass instance)\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.popOutManager) {\n magnifyGlass.popOutManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]);\n }\n });\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/PopOutManager.js b/web/magnify-glass/PopOutManager.js index ad4b840..55579f8 100644 --- a/web/magnify-glass/PopOutManager.js +++ b/web/magnify-glass/PopOutManager.js @@ -10,6 +10,8 @@ class PopOutManager { __publicField(this, "lastPongTime", 0); __publicField(this, "pingInterval", null); __publicField(this, "viewerUrl"); + __publicField(this, "currentTheme", "dark"); + // Default theme /** Throttle frame sending to ~30fps */ __publicField(this, "lastFrameTime", 0); __publicField(this, "FRAME_INTERVAL", 33); @@ -72,6 +74,7 @@ class PopOutManager { if (!this.isOpen) { this.isOpen = true; Logger.debug("[PopOut] Viewer tab connected"); + this.sendConfig({ theme: this.currentTheme }); } break; case "close": @@ -146,16 +149,36 @@ class PopOutManager { Logger.error("[PopOut] Failed to send frame:", e); } } + /** + * Send configuration to the pop-out viewer. + */ /** * Send configuration to the pop-out viewer. */ sendConfig(config) { if (!this.channel) return; + const finalConfig = { + theme: this.currentTheme, + ...config + }; + if (config.theme) { + this.currentTheme = config.theme; + } this.sendMessage({ type: "config", - data: config + data: finalConfig }); } + /** + * Update the viewer theme. + * @param theme - Theme name (e.g. 'dark', 'light') + */ + updateTheme(theme) { + if (this.currentTheme === theme) return; + this.currentTheme = theme; + this.sendConfig({ theme }); + Logger.debug(`[PopOut] Theme updated to: ${theme}`); + } /** * Send inspector info to the pop-out viewer. * @param info - Inspector panel information diff --git a/web/magnify-glass/PopOutManager.js.map b/web/magnify-glass/PopOutManager.js.map index 31f4636..7c43432 100644 --- a/web/magnify-glass/PopOutManager.js.map +++ b/web/magnify-glass/PopOutManager.js.map @@ -1 +1 @@ -{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong';\n data?: string | PopOutConfig | PopOutInfo;\n timestamp?: number;\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n executionOrder?: number;\n category?: string;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v4';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n }\n break;\n case 'close':\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Viewer tab closed');\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n if (this.isOpen) {\n Logger.debug('[PopOut] Tab already open');\n return;\n }\n\n // Open new tab\n const newTab = window.open(this.viewerUrl, '_blank');\n if (!newTab) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen) return;\n\n this.sendMessage({ type: 'close' });\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: PopOutConfig): void {\n if (!this.channel) return;\n\n this.sendMessage({\n type: 'config',\n data: config\n });\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n this.stopPing();\n this.pingInterval = setInterval(() => {\n this.sendMessage({ type: 'ping', timestamp: Date.now() });\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing();\n }\n }, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong';\n data?: string | Partial | PopOutInfo;\n timestamp?: number;\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n theme?: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n executionOrder?: number;\n category?: string;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n private currentTheme: string = 'dark'; // Default theme\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v4';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n // Send current theme immediately upon connection\n this.sendConfig({ theme: this.currentTheme });\n }\n break;\n case 'close':\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Viewer tab closed');\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n if (this.isOpen) {\n Logger.debug('[PopOut] Tab already open');\n return;\n }\n\n // Open new tab\n const newTab = window.open(this.viewerUrl, '_blank');\n if (!newTab) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen) return;\n\n this.sendMessage({ type: 'close' });\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: Partial): void {\n if (!this.channel) return;\n\n // Merge with current theme if not provided\n const finalConfig = {\n theme: this.currentTheme,\n ...config\n };\n\n // Update local theme cache if provided in config\n if (config.theme) {\n this.currentTheme = config.theme;\n }\n\n this.sendMessage({\n type: 'config',\n data: finalConfig\n });\n }\n\n /**\n * Update the viewer theme.\n * @param theme - Theme name (e.g. 'dark', 'light')\n */\n updateTheme(theme: string): void {\n if (this.currentTheme === theme) return;\n\n this.currentTheme = theme;\n this.sendConfig({ theme });\n Logger.debug(`[PopOut] Theme updated to: ${theme}`);\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n this.stopPing();\n this.pingInterval = setInterval(() => {\n this.sendMessage({ type: 'ping', timestamp: Date.now() });\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing();\n }\n }, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/popout-viewer.html b/web/popout-viewer.html index d96fb15..bc630f6 100644 --- a/web/popout-viewer.html +++ b/web/popout-viewer.html @@ -32,6 +32,129 @@ --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px; + --radius-lg: 14px; + } + + /* Light Theme Overrides */ + [data-theme="light"] { + --bg-primary: #f9f9fb; + --bg-secondary: #ffffff; + --bg-tertiary: #f3f4f6; + --bg-elevated: #e5e7eb; + --border-subtle: rgba(0, 0, 0, 0.05); + --border-default: rgba(0, 0, 0, 0.1); + --border-accent: rgba(99, 102, 241, 0.3); + --text-primary: #111827; + --text-secondary: #4b5563; + --text-tertiary: #9ca3af; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + } + + /* Solarized Theme */ + [data-theme="solarized"] { + --bg-primary: #002b36; + --bg-secondary: #073642; + --bg-tertiary: #073642; + --bg-elevated: #002b36; + --border-subtle: rgba(88, 110, 117, 0.3); + --border-default: rgba(88, 110, 117, 0.5); + --text-primary: #839496; + --text-secondary: #586e75; + --text-tertiary: #586e75; + --accent-blue: #268bd2; + --accent-cyan: #2aa198; + --accent-green: #859900; + } + + /* Arc Theme */ + [data-theme="arc"] { + --bg-primary: #383c4a; + --bg-secondary: #404552; + --bg-tertiary: #2f343f; + --bg-elevated: #4b5162; + --border-subtle: rgba(0, 0, 0, 0.2); + --border-default: rgba(0, 0, 0, 0.3); + --text-primary: #d3dae3; + --text-secondary: #a0a8b9; + --text-tertiary: #7c818c; + --accent-blue: #5294e2; + } + + /* Nord Theme */ + [data-theme="nord"] { + --bg-primary: #2e3440; + --bg-secondary: #3b4252; + --bg-tertiary: #434c5e; + --bg-elevated: #4c566a; + --border-subtle: rgba(0, 0, 0, 0.2); + --border-default: rgba(236, 239, 244, 0.1); + --text-primary: #d8dee9; + --text-secondary: #e5e9f0; + --text-tertiary: #4c566a; + --accent-blue: #81a1c1; + --accent-cyan: #88c0d0; + --accent-green: #a3be8c; + } + + /* GitHub Theme */ + [data-theme="github"] { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #ffffff; + --bg-elevated: #f6f8fa; + --border-subtle: rgba(27, 31, 35, 0.05); + --border-default: rgba(27, 31, 35, 0.1); + --text-primary: #24292e; + --text-secondary: #586069; + --text-tertiary: #6a737d; + --accent-blue: #0366d6; + } + + /* Gruvbox Theme */ + [data-theme="gruvbox"] { + --bg-primary: #282828; + --bg-secondary: #3c3836; + --bg-tertiary: #504945; + --bg-elevated: #665c54; + --border-subtle: rgba(168, 153, 132, 0.2); + --border-default: rgba(168, 153, 132, 0.4); + --text-primary: #ebdbb2; + --text-secondary: #a89984; + --text-tertiary: #928374; + --accent-blue: #83a598; + --accent-green: #b8bb26; + } + + /* Dracula Theme */ + [data-theme="dracula"] { + --bg-primary: #282a36; + --bg-secondary: #44475a; + --bg-tertiary: #44475a; + --bg-elevated: #6272a4; + --border-subtle: rgba(189, 147, 249, 0.2); + --border-default: rgba(98, 114, 164, 0.5); + --text-primary: #f8f8f2; + --text-secondary: #6272a4; + --text-tertiary: #6272a4; + --accent-blue: #8be9fd; + --accent-green: #50fa7b; + } + + /* Monokai Theme */ + [data-theme="monokai"] { + --bg-primary: #272822; + --bg-secondary: #3e3d32; + --bg-tertiary: #3e3d32; + --bg-elevated: #49483e; + --border-subtle: rgba(255, 255, 255, 0.1); + --border-default: rgba(255, 255, 255, 0.2); + --text-primary: #f8f8f2; + --text-secondary: #75715e; + --text-tertiary: #75715e; + --accent-blue: #66d9ef; + --accent-green: #a6e22e; } * { @@ -621,6 +744,10 @@ } else { canvas.style.borderRadius = 'var(--radius-lg)'; } + + if (config.theme) { + document.documentElement.setAttribute('data-theme', config.theme); + } } function handleInfo(info) {