diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts index 87ead0f..170a483 100644 --- a/src/magnify-glass/PopOutManager.ts +++ b/src/magnify-glass/PopOutManager.ts @@ -101,7 +101,7 @@ export class PopOutManager { */ private getViewerUrl(): string { // Cache-busting version - increment to force refresh - const version = 'v21'; + const version = 'v22'; // Find the extension's base URL from the loaded scripts const scripts = document.querySelectorAll('script[src*="magnify"]'); diff --git a/web/magnify-glass/PopOutManager.js b/web/magnify-glass/PopOutManager.js index 31e2613..524b6c3 100644 --- a/web/magnify-glass/PopOutManager.js +++ b/web/magnify-glass/PopOutManager.js @@ -28,7 +28,7 @@ class PopOutManager { * Get the URL for the pop-out viewer page. */ getViewerUrl() { - const version = "v21"; + const version = "v22"; const scripts = document.querySelectorAll('script[src*="magnify"]'); Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`); if (scripts.length > 0) { diff --git a/web/magnify-glass/PopOutManager.js.map b/web/magnify-glass/PopOutManager.js.map index b46b8b1..c1ca82a 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' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node';\n data?: string | Partial | PopOutInfo | NodeListData | number;\n timestamp?: number;\n}\n\n/**\n * Node list data sent to popout for dropdown\n */\ninterface NodeListData {\n nodes: { id: number; title: string; type: string; order?: number }[];\n sortBy: 'title' | 'execOrder' | 'id';\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 id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\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 popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n public onNodeSelect: ((nodeId: number) => void) | null = null;\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 = 'v21';\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 // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\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 if (this.onStateChange) this.onStateChange(true);\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.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n case 'request-nodes':\n // Popout is requesting the node list for dropdown\n Logger.debug('[PopOut] Received request-nodes:', message.data);\n this.handleNodeListRequest(message.data as 'title' | 'execOrder' | 'id');\n break;\n case 'node-select':\n // Popout selected a node, notify main window\n if (this.onNodeSelect && typeof message.data === 'number') {\n this.onNodeSelect(message.data);\n }\n break;\n case 'zoom-node':\n // Popout requested to zoom to a node\n if (typeof message.data === 'number') {\n const app = (window as any).app;\n const node = app.graph.getNodeById(message.data);\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\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 && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\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 * Handle node list request from popout viewer.\n * Fetches nodes from canvas and sends them to the popout.\n */\n private handleNodeListRequest(sortBy: 'title' | 'execOrder' | 'id'): void {\n if (!this.channel) return;\n\n try {\n // Access nodes from ComfyUI's app object\n const app = (window as any).app;\n const nodes = app?.graph?._nodes || [];\n\n let nodeList: { id: number; title: string; type: string; order?: number }[];\n\n if (sortBy === 'execOrder') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown',\n order: n.order ?? -1\n }))\n .filter((n: any) => n.order >= 0)\n .sort((a: any, b: any) => a.order - b.order);\n } else if (sortBy === 'id') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.id - b.id);\n } else {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.title.localeCompare(b.title));\n }\n\n this.sendMessage({\n type: 'nodes-list',\n data: { nodes: nodeList, sortBy }\n });\n\n Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);\n } catch (e) {\n Logger.error('[PopOut] Failed to get node list:', e);\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 id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\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 * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Sanitized options for widgets\n if (item.options) {\n safeItem.options = this.sanitizeOptions(item.options);\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\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 // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\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(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 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' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node';\n data?: string | Partial | PopOutInfo | NodeListData | number;\n timestamp?: number;\n}\n\n/**\n * Node list data sent to popout for dropdown\n */\ninterface NodeListData {\n nodes: { id: number; title: string; type: string; order?: number }[];\n sortBy: 'title' | 'execOrder' | 'id';\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 id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\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 popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n public onNodeSelect: ((nodeId: number) => void) | null = null;\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 = 'v22';\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 // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\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 if (this.onStateChange) this.onStateChange(true);\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.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n case 'request-nodes':\n // Popout is requesting the node list for dropdown\n Logger.debug('[PopOut] Received request-nodes:', message.data);\n this.handleNodeListRequest(message.data as 'title' | 'execOrder' | 'id');\n break;\n case 'node-select':\n // Popout selected a node, notify main window\n if (this.onNodeSelect && typeof message.data === 'number') {\n this.onNodeSelect(message.data);\n }\n break;\n case 'zoom-node':\n // Popout requested to zoom to a node\n if (typeof message.data === 'number') {\n const app = (window as any).app;\n const node = app.graph.getNodeById(message.data);\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\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 && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\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 * Handle node list request from popout viewer.\n * Fetches nodes from canvas and sends them to the popout.\n */\n private handleNodeListRequest(sortBy: 'title' | 'execOrder' | 'id'): void {\n if (!this.channel) return;\n\n try {\n // Access nodes from ComfyUI's app object\n const app = (window as any).app;\n const nodes = app?.graph?._nodes || [];\n\n let nodeList: { id: number; title: string; type: string; order?: number }[];\n\n if (sortBy === 'execOrder') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown',\n order: n.order ?? -1\n }))\n .filter((n: any) => n.order >= 0)\n .sort((a: any, b: any) => a.order - b.order);\n } else if (sortBy === 'id') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.id - b.id);\n } else {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.title.localeCompare(b.title));\n }\n\n this.sendMessage({\n type: 'nodes-list',\n data: { nodes: nodeList, sortBy }\n });\n\n Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);\n } catch (e) {\n Logger.error('[PopOut] Failed to get node list:', e);\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 id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\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 * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Sanitized options for widgets\n if (item.options) {\n safeItem.options = this.sanitizeOptions(item.options);\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\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 // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\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(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 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 ef48b1e..c7277bc 100644 --- a/web/popout-viewer.html +++ b/web/popout-viewer.html @@ -1196,21 +1196,121 @@ function renderList(title, items) { if (!items || items.length === 0) return ''; - const listHtml = items.map(item => { - const name = escapeHtml(item.name || item.label || 'Unknown'); - const type = item.type ? `(${escapeHtml(item.type)})` : ''; - const value = item.value !== undefined ? `${escapeHtml(String(item.value))}` : ''; + // Icon mapping for different list types + const icons = { + 'Inputs': ``, + 'Outputs': ``, + 'Widgets': `` + }; + const icon = icons[title] || ''; - return `
- - ${name}${type} - ${value} -
`; + // Gradient colors for different types + const gradients = { + 'Inputs': 'linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.05))', + 'Outputs': 'linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.05))', + 'Widgets': 'linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(168, 85, 247, 0.05))' + }; + const gradient = gradients[title] || 'linear-gradient(135deg, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.05))'; + + // Badge colors for different types + const badgeColors = { + 'Inputs': '#3b82f6', + 'Outputs': '#22c55e', + 'Widgets': '#a855f7' + }; + const badgeColor = badgeColors[title] || '#888'; + + const listHtml = items.map((item, index) => { + const name = escapeHtml(item.name || item.label || 'Unknown'); + const type = item.type ? escapeHtml(item.type) : null; + const value = item.value !== undefined ? escapeHtml(String(item.value)) : null; + + return ` +
+
+ ${name} + ${type ? `${type}` : ''} +
+ ${value ? `${value}` : ''} +
`; }).join(''); return ` -
-
${title} (${items.length})
-
${listHtml}
+
+
+ ${icon} + ${title} + ${items.length} +
+
+ ${listHtml} +
`; }