diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts index 468ee6d..932e586 100644 --- a/src/magnify-glass/PopOutManager.ts +++ b/src/magnify-glass/PopOutManager.ts @@ -84,7 +84,7 @@ export class PopOutManager { */ private getViewerUrl(): string { // Cache-busting version - increment to force refresh - const version = 'v16'; + const version = 'v17'; // 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 a629646..7e7514b 100644 --- a/web/magnify-glass/PopOutManager.js +++ b/web/magnify-glass/PopOutManager.js @@ -27,7 +27,7 @@ class PopOutManager { * Get the URL for the pop-out viewer page. */ getViewerUrl() { - const version = "v16"; + const version = "v17"; 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 16ae73b..e1c3710 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 | 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 pythonModule?: 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 popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => 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 = 'v16';\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 }\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 * 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 pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : 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 // 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';\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 pythonModule?: 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 popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => 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 = 'v17';\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 }\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 * 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 pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : 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 // 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 42432fe..8f75c03 100644 --- a/web/popout-viewer.html +++ b/web/popout-viewer.html @@ -984,11 +984,6 @@ Scale ${((info.canvas.scale || 1) * 100).toFixed(1)}% - ${info.magnifier ? ` -
- Zoom - ${info.magnifier.zoomFactor || 1}× -
` : ''} `;