feat: Add pop-out viewer for multi-monitor magnify glass display.
This commit is contained in:
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.7.0] - 2026-01-02
|
||||
|
||||
### Added
|
||||
- **Multi-Monitor Pop-Out Tab**: Open the magnified view in a separate browser tab that can be detached and moved to a secondary monitor.
|
||||
- Pop-out button appears on hover in the top-right corner of the glass.
|
||||
- Keyboard shortcut `Shift+P` to toggle pop-out when magnifier is active.
|
||||
- Real-time sync at 30fps using BroadcastChannel API.
|
||||
- Professional dark-themed viewer with connection status indicator.
|
||||
- Viewer page automatically syncs glass shape and border styling.
|
||||
|
||||
---
|
||||
|
||||
## [1.6.0] - 2025-01-02
|
||||
|
||||
### Added
|
||||
|
||||
@@ -152,6 +152,12 @@ export class EventHandler {
|
||||
config.saveOffsets();
|
||||
this.magnifyGlass.updateMagnifiedView();
|
||||
}
|
||||
|
||||
// Pop-out tab toggle (Shift+P)
|
||||
if (e.shiftKey && e.key.toLowerCase() === 'p') {
|
||||
this.magnifyGlass.popOutManager.toggle();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { UiManager } from './UiManager';
|
||||
import { WebGLRenderer } from './WebGLRenderer';
|
||||
import { DebugManager } from './DebugManager';
|
||||
import { EventHandler } from './EventHandler';
|
||||
import { PopOutManager } from './PopOutManager';
|
||||
|
||||
// External ComfyUI globals
|
||||
declare const app: ComfyApp;
|
||||
@@ -30,6 +31,7 @@ export class MagnifyGlass {
|
||||
renderer: WebGLRenderer | null;
|
||||
debugger: DebugManager;
|
||||
eventHandler: EventHandler;
|
||||
popOutManager: PopOutManager;
|
||||
|
||||
/** The LiteGraph canvas */
|
||||
litegraphCanvas: HTMLCanvasElement | null;
|
||||
@@ -46,7 +48,13 @@ export class MagnifyGlass {
|
||||
constructor() {
|
||||
this.config = new ConfigManager();
|
||||
this.state = new MagnifierState();
|
||||
this.ui = new UiManager(this.config, this.state, () => this.toggle());
|
||||
this.popOutManager = new PopOutManager();
|
||||
this.ui = new UiManager(
|
||||
this.config,
|
||||
this.state,
|
||||
() => this.toggle(),
|
||||
() => this.popOutManager.toggle()
|
||||
);
|
||||
this.renderer = null;
|
||||
this.debugger = new DebugManager(this.config, this.state, this.ui);
|
||||
this.eventHandler = new EventHandler(this);
|
||||
@@ -162,6 +170,11 @@ export class MagnifyGlass {
|
||||
// Render HTML overlays
|
||||
this.renderHtmlOverlays();
|
||||
|
||||
// Send frame to pop-out tab if open
|
||||
if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) {
|
||||
this.popOutManager.sendFrame(this.ui.glassCanvas);
|
||||
}
|
||||
|
||||
this.state.isRenderScheduled = false;
|
||||
});
|
||||
}
|
||||
@@ -440,6 +453,7 @@ export class MagnifyGlass {
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.eventHandler.detachListeners();
|
||||
this.popOutManager.cleanup();
|
||||
this.ui.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* ComfyUI MagnifyGlass - PopOutManager (TypeScript)
|
||||
*
|
||||
* Manages pop-out tab functionality for multi-monitor viewing.
|
||||
* Uses BroadcastChannel API for efficient cross-tab communication.
|
||||
*/
|
||||
|
||||
import { BROADCAST_CHANNEL_NAME } from '../shared/constants';
|
||||
import { Logger } from '../shared/logger';
|
||||
|
||||
/**
|
||||
* Message types for BroadcastChannel communication
|
||||
*/
|
||||
interface PopOutMessage {
|
||||
type: 'frame' | 'config' | 'close' | 'ping' | 'pong';
|
||||
data?: string | PopOutConfig;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
interface PopOutConfig {
|
||||
glassSize: number;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
glassShape: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* PopOutManager class.
|
||||
* Handles opening, closing, and syncing with an external browser tab.
|
||||
*/
|
||||
export class PopOutManager {
|
||||
private channel: BroadcastChannel | null = null;
|
||||
private isOpen: boolean = false;
|
||||
private lastPongTime: number = 0;
|
||||
private pingInterval: number | null = null;
|
||||
private viewerUrl: string;
|
||||
|
||||
/** Throttle frame sending to ~30fps */
|
||||
private lastFrameTime: number = 0;
|
||||
private readonly FRAME_INTERVAL = 33; // ~30fps
|
||||
|
||||
/** Connection timeout (ms) */
|
||||
private readonly CONNECTION_TIMEOUT = 5000;
|
||||
|
||||
constructor() {
|
||||
// Get the viewer URL relative to the extension
|
||||
// The viewer HTML will be in the web directory
|
||||
this.viewerUrl = this.getViewerUrl();
|
||||
this.initChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the pop-out viewer page.
|
||||
*/
|
||||
private getViewerUrl(): string {
|
||||
// Find the extension's base URL from the loaded scripts
|
||||
const scripts = document.querySelectorAll('script[src*="magnify"]');
|
||||
if (scripts.length > 0) {
|
||||
const src = (scripts[0] as HTMLScriptElement).src;
|
||||
const baseUrl = src.substring(0, src.lastIndexOf('/'));
|
||||
return `${baseUrl}/popout-viewer.html`;
|
||||
}
|
||||
// Fallback: assume standard ComfyUI extension path
|
||||
return '/extensions/comfyui-magnifyglass/popout-viewer.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the BroadcastChannel.
|
||||
*/
|
||||
private initChannel(): void {
|
||||
try {
|
||||
this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
||||
this.channel.onmessage = (event: MessageEvent<PopOutMessage>) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
Logger.debug('[PopOut] BroadcastChannel initialized');
|
||||
} catch (e) {
|
||||
Logger.error('[PopOut] Failed to create BroadcastChannel:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages from the viewer tab.
|
||||
*/
|
||||
private handleMessage(message: PopOutMessage): void {
|
||||
switch (message.type) {
|
||||
case 'pong':
|
||||
this.lastPongTime = Date.now();
|
||||
if (!this.isOpen) {
|
||||
this.isOpen = true;
|
||||
Logger.debug('[PopOut] Viewer tab connected');
|
||||
}
|
||||
break;
|
||||
case 'close':
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
Logger.debug('[PopOut] Viewer tab closed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the pop-out viewer in a new tab.
|
||||
*/
|
||||
open(): void {
|
||||
if (this.isOpen) {
|
||||
Logger.debug('[PopOut] Tab already open');
|
||||
return;
|
||||
}
|
||||
|
||||
// Open new tab
|
||||
const newTab = window.open(this.viewerUrl, '_blank');
|
||||
if (!newTab) {
|
||||
Logger.error('[PopOut] Failed to open new tab - popup may be blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start pinging to detect when viewer is ready
|
||||
this.startPing();
|
||||
Logger.debug('[PopOut] Opening viewer tab...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the pop-out viewer tab.
|
||||
*/
|
||||
close(): void {
|
||||
if (!this.isOpen) return;
|
||||
|
||||
this.sendMessage({ type: 'close' });
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
Logger.debug('[PopOut] Sent close message to viewer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the pop-out state.
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the pop-out tab is currently open.
|
||||
*/
|
||||
isPopOutOpen(): boolean {
|
||||
return this.isOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a frame to the pop-out viewer.
|
||||
* Throttled to ~30fps for performance.
|
||||
* @param canvas - The source canvas to send
|
||||
*/
|
||||
sendFrame(canvas: HTMLCanvasElement): void {
|
||||
if (!this.isOpen || !this.channel) return;
|
||||
|
||||
// Throttle frame rate
|
||||
const now = Date.now();
|
||||
if (now - this.lastFrameTime < this.FRAME_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
this.lastFrameTime = now;
|
||||
|
||||
try {
|
||||
// Convert canvas to data URL (efficient for small canvases)
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
this.sendMessage({
|
||||
type: 'frame',
|
||||
data: dataUrl,
|
||||
timestamp: now
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.error('[PopOut] Failed to send frame:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send configuration to the pop-out viewer.
|
||||
*/
|
||||
sendConfig(config: PopOutConfig): void {
|
||||
if (!this.channel) return;
|
||||
|
||||
this.sendMessage({
|
||||
type: 'config',
|
||||
data: config
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message through the BroadcastChannel.
|
||||
*/
|
||||
private sendMessage(message: PopOutMessage): void {
|
||||
if (!this.channel) return;
|
||||
|
||||
try {
|
||||
this.channel.postMessage(message);
|
||||
} catch (e) {
|
||||
Logger.error('[PopOut] Failed to send message:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start pinging to check connection.
|
||||
*/
|
||||
private startPing(): void {
|
||||
this.stopPing();
|
||||
this.pingInterval = setInterval(() => {
|
||||
this.sendMessage({ type: 'ping', timestamp: Date.now() });
|
||||
|
||||
// Check for connection timeout
|
||||
if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {
|
||||
Logger.debug('[PopOut] Connection timeout, marking as closed');
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop pinging.
|
||||
*/
|
||||
private stopPing(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources.
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.close();
|
||||
this.stopPing();
|
||||
if (this.channel) {
|
||||
this.channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
Logger.debug('[PopOut] Cleaned up');
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,11 @@ export class UiManager {
|
||||
debugCanvas: HTMLCanvasElement | null;
|
||||
debugCtx: CanvasRenderingContext2D | null;
|
||||
htmlOverlayContainer: HTMLDivElement | null;
|
||||
popOutButton: HTMLButtonElement | null;
|
||||
onToggle: (() => void) | undefined;
|
||||
onPopOut: (() => void) | undefined;
|
||||
|
||||
constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void) {
|
||||
constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void, onPopOut?: () => void) {
|
||||
this.config = config;
|
||||
this.state = state;
|
||||
this.glassDiv = null;
|
||||
@@ -32,7 +34,9 @@ export class UiManager {
|
||||
this.debugCanvas = null;
|
||||
this.debugCtx = null;
|
||||
this.htmlOverlayContainer = null;
|
||||
this.popOutButton = null;
|
||||
this.onToggle = onToggle;
|
||||
this.onPopOut = onPopOut;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +82,63 @@ export class UiManager {
|
||||
`;
|
||||
this.glassDiv.appendChild(this.htmlOverlayContainer);
|
||||
|
||||
// Create pop-out button
|
||||
this.popOutButton = document.createElement("button");
|
||||
this.popOutButton.id = "comfyui-magnify-popout-btn";
|
||||
this.popOutButton.title = "Open in New Tab (Shift+P)";
|
||||
this.popOutButton.innerHTML = Icons.externalLink;
|
||||
this.popOutButton.style.cssText = `
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||
z-index: 10;
|
||||
`;
|
||||
this.popOutButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.onPopOut) {
|
||||
this.onPopOut();
|
||||
}
|
||||
});
|
||||
this.popOutButton.addEventListener('mouseenter', () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.background = 'rgba(99, 102, 241, 0.6)';
|
||||
this.popOutButton.style.color = '#fff';
|
||||
}
|
||||
});
|
||||
this.popOutButton.addEventListener('mouseleave', () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.background = 'rgba(0, 0, 0, 0.4)';
|
||||
this.popOutButton.style.color = 'rgba(255, 255, 255, 0.7)';
|
||||
}
|
||||
});
|
||||
this.glassDiv.appendChild(this.popOutButton);
|
||||
|
||||
// Show pop-out button on glass hover
|
||||
this.glassDiv.addEventListener('mouseenter', () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
this.glassDiv.addEventListener('mouseleave', () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(this.glassDiv);
|
||||
|
||||
// Create debug canvas if debug mode is enabled
|
||||
|
||||
@@ -84,3 +84,7 @@ export const STORAGE_KEYS = {
|
||||
OFFSET_X: 'comfyui_magnify_offset_x',
|
||||
OFFSET_Y: 'comfyui_magnify_offset_y'
|
||||
} as const;
|
||||
|
||||
// BroadcastChannel for pop-out tab communication
|
||||
export const BROADCAST_CHANNEL_NAME = 'comfyui-magnifyglass-popout';
|
||||
|
||||
|
||||
@@ -82,6 +82,13 @@ export const Icons = {
|
||||
// Magnet icon - persist/sticky mode
|
||||
magnet: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 15-4-4 6.75-6.77a7.79 7.79 0 0 1 11 11L13 22l-4-4 6.39-6.36a2.2 2.2 0 0 0-3.1-3.13l-6.29 6.49Z"/>
|
||||
</svg>`,
|
||||
|
||||
// External link icon - pop out to new tab
|
||||
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
|
||||
@@ -117,6 +117,10 @@ class EventHandler {
|
||||
config.saveOffsets();
|
||||
this.magnifyGlass.updateMagnifiedView();
|
||||
}
|
||||
if (e.shiftKey && e.key.toLowerCase() === "p") {
|
||||
this.magnifyGlass.popOutManager.toggle();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ import { UiManager } from "./UiManager.js";
|
||||
import { WebGLRenderer } from "./WebGLRenderer.js";
|
||||
import { DebugManager } from "./DebugManager.js";
|
||||
import { EventHandler } from "./EventHandler.js";
|
||||
import { PopOutManager } from "./PopOutManager.js";
|
||||
class MagnifyGlass {
|
||||
constructor() {
|
||||
__publicField(this, "config");
|
||||
@@ -19,6 +20,7 @@ class MagnifyGlass {
|
||||
__publicField(this, "renderer");
|
||||
__publicField(this, "debugger");
|
||||
__publicField(this, "eventHandler");
|
||||
__publicField(this, "popOutManager");
|
||||
/** The LiteGraph canvas */
|
||||
__publicField(this, "litegraphCanvas");
|
||||
/** Last known mouse position for better initial positioning */
|
||||
@@ -29,7 +31,13 @@ class MagnifyGlass {
|
||||
__publicField(this, "currentMediaElement");
|
||||
this.config = new ConfigManager();
|
||||
this.state = new MagnifierState();
|
||||
this.ui = new UiManager(this.config, this.state, () => this.toggle());
|
||||
this.popOutManager = new PopOutManager();
|
||||
this.ui = new UiManager(
|
||||
this.config,
|
||||
this.state,
|
||||
() => this.toggle(),
|
||||
() => this.popOutManager.toggle()
|
||||
);
|
||||
this.renderer = null;
|
||||
this.debugger = new DebugManager(this.config, this.state, this.ui);
|
||||
this.eventHandler = new EventHandler(this);
|
||||
@@ -104,6 +112,9 @@ class MagnifyGlass {
|
||||
this.renderer.render(this.litegraphCanvas);
|
||||
this.debugger.updateDebugView();
|
||||
this.renderHtmlOverlays();
|
||||
if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) {
|
||||
this.popOutManager.sendFrame(this.ui.glassCanvas);
|
||||
}
|
||||
this.state.isRenderScheduled = false;
|
||||
});
|
||||
}
|
||||
@@ -330,6 +341,7 @@ class MagnifyGlass {
|
||||
*/
|
||||
cleanup() {
|
||||
this.eventHandler.detachListeners();
|
||||
this.popOutManager.cleanup();
|
||||
this.ui.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||||
import { BROADCAST_CHANNEL_NAME } from "../shared/constants.js";
|
||||
import { Logger } from "../shared/logger.js";
|
||||
class PopOutManager {
|
||||
constructor() {
|
||||
__publicField(this, "channel", null);
|
||||
__publicField(this, "isOpen", false);
|
||||
__publicField(this, "lastPongTime", 0);
|
||||
__publicField(this, "pingInterval", null);
|
||||
__publicField(this, "viewerUrl");
|
||||
/** Throttle frame sending to ~30fps */
|
||||
__publicField(this, "lastFrameTime", 0);
|
||||
__publicField(this, "FRAME_INTERVAL", 33);
|
||||
// ~30fps
|
||||
/** Connection timeout (ms) */
|
||||
__publicField(this, "CONNECTION_TIMEOUT", 5e3);
|
||||
this.viewerUrl = this.getViewerUrl();
|
||||
this.initChannel();
|
||||
}
|
||||
/**
|
||||
* Get the URL for the pop-out viewer page.
|
||||
*/
|
||||
getViewerUrl() {
|
||||
const scripts = document.querySelectorAll('script[src*="magnify"]');
|
||||
if (scripts.length > 0) {
|
||||
const src = scripts[0].src;
|
||||
const baseUrl = src.substring(0, src.lastIndexOf("/"));
|
||||
return `${baseUrl}/popout-viewer.html`;
|
||||
}
|
||||
return "/extensions/comfyui-magnifyglass/popout-viewer.html";
|
||||
}
|
||||
/**
|
||||
* Initialize the BroadcastChannel.
|
||||
*/
|
||||
initChannel() {
|
||||
try {
|
||||
this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
||||
this.channel.onmessage = (event) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
Logger.debug("[PopOut] BroadcastChannel initialized");
|
||||
} catch (e) {
|
||||
Logger.error("[PopOut] Failed to create BroadcastChannel:", e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle incoming messages from the viewer tab.
|
||||
*/
|
||||
handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case "pong":
|
||||
this.lastPongTime = Date.now();
|
||||
if (!this.isOpen) {
|
||||
this.isOpen = true;
|
||||
Logger.debug("[PopOut] Viewer tab connected");
|
||||
}
|
||||
break;
|
||||
case "close":
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
Logger.debug("[PopOut] Viewer tab closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Open the pop-out viewer in a new tab.
|
||||
*/
|
||||
open() {
|
||||
if (this.isOpen) {
|
||||
Logger.debug("[PopOut] Tab already open");
|
||||
return;
|
||||
}
|
||||
const newTab = window.open(this.viewerUrl, "_blank");
|
||||
if (!newTab) {
|
||||
Logger.error("[PopOut] Failed to open new tab - popup may be blocked");
|
||||
return;
|
||||
}
|
||||
this.startPing();
|
||||
Logger.debug("[PopOut] Opening viewer tab...");
|
||||
}
|
||||
/**
|
||||
* Close the pop-out viewer tab.
|
||||
*/
|
||||
close() {
|
||||
if (!this.isOpen) return;
|
||||
this.sendMessage({ type: "close" });
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
Logger.debug("[PopOut] Sent close message to viewer");
|
||||
}
|
||||
/**
|
||||
* Toggle the pop-out state.
|
||||
*/
|
||||
toggle() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if the pop-out tab is currently open.
|
||||
*/
|
||||
isPopOutOpen() {
|
||||
return this.isOpen;
|
||||
}
|
||||
/**
|
||||
* Send a frame to the pop-out viewer.
|
||||
* Throttled to ~30fps for performance.
|
||||
* @param canvas - The source canvas to send
|
||||
*/
|
||||
sendFrame(canvas) {
|
||||
if (!this.isOpen || !this.channel) return;
|
||||
const now = Date.now();
|
||||
if (now - this.lastFrameTime < this.FRAME_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
this.lastFrameTime = now;
|
||||
try {
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
this.sendMessage({
|
||||
type: "frame",
|
||||
data: dataUrl,
|
||||
timestamp: now
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.error("[PopOut] Failed to send frame:", e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send configuration to the pop-out viewer.
|
||||
*/
|
||||
sendConfig(config) {
|
||||
if (!this.channel) return;
|
||||
this.sendMessage({
|
||||
type: "config",
|
||||
data: config
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Send a message through the BroadcastChannel.
|
||||
*/
|
||||
sendMessage(message) {
|
||||
if (!this.channel) return;
|
||||
try {
|
||||
this.channel.postMessage(message);
|
||||
} catch (e) {
|
||||
Logger.error("[PopOut] Failed to send message:", e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Start pinging to check connection.
|
||||
*/
|
||||
startPing() {
|
||||
this.stopPing();
|
||||
this.pingInterval = setInterval(() => {
|
||||
this.sendMessage({ type: "ping", timestamp: Date.now() });
|
||||
if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {
|
||||
Logger.debug("[PopOut] Connection timeout, marking as closed");
|
||||
this.isOpen = false;
|
||||
this.stopPing();
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
/**
|
||||
* Stop pinging.
|
||||
*/
|
||||
stopPing() {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup resources.
|
||||
*/
|
||||
cleanup() {
|
||||
this.close();
|
||||
this.stopPing();
|
||||
if (this.channel) {
|
||||
this.channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
Logger.debug("[PopOut] Cleaned up");
|
||||
}
|
||||
}
|
||||
export {
|
||||
PopOutManager
|
||||
};
|
||||
//# sourceMappingURL=PopOutManager.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ import { Z_INDEX, DEFAULT_PADDING } from "../shared/constants.js";
|
||||
import { Icons } from "../shared/icons.js";
|
||||
import { Logger } from "../shared/logger.js";
|
||||
class UiManager {
|
||||
constructor(config, state, onToggle) {
|
||||
constructor(config, state, onToggle, onPopOut) {
|
||||
__publicField(this, "config");
|
||||
__publicField(this, "state");
|
||||
__publicField(this, "glassDiv");
|
||||
@@ -13,7 +13,9 @@ class UiManager {
|
||||
__publicField(this, "debugCanvas");
|
||||
__publicField(this, "debugCtx");
|
||||
__publicField(this, "htmlOverlayContainer");
|
||||
__publicField(this, "popOutButton");
|
||||
__publicField(this, "onToggle");
|
||||
__publicField(this, "onPopOut");
|
||||
this.config = config;
|
||||
this.state = state;
|
||||
this.glassDiv = null;
|
||||
@@ -21,7 +23,9 @@ class UiManager {
|
||||
this.debugCanvas = null;
|
||||
this.debugCtx = null;
|
||||
this.htmlOverlayContainer = null;
|
||||
this.popOutButton = null;
|
||||
this.onToggle = onToggle;
|
||||
this.onPopOut = onPopOut;
|
||||
}
|
||||
/**
|
||||
* Create all DOM elements for the magnifying glass.
|
||||
@@ -60,6 +64,59 @@ class UiManager {
|
||||
overflow: hidden;
|
||||
`;
|
||||
this.glassDiv.appendChild(this.htmlOverlayContainer);
|
||||
this.popOutButton = document.createElement("button");
|
||||
this.popOutButton.id = "comfyui-magnify-popout-btn";
|
||||
this.popOutButton.title = "Open in New Tab (Shift+P)";
|
||||
this.popOutButton.innerHTML = Icons.externalLink;
|
||||
this.popOutButton.style.cssText = `
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||
z-index: 10;
|
||||
`;
|
||||
this.popOutButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.onPopOut) {
|
||||
this.onPopOut();
|
||||
}
|
||||
});
|
||||
this.popOutButton.addEventListener("mouseenter", () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.background = "rgba(99, 102, 241, 0.6)";
|
||||
this.popOutButton.style.color = "#fff";
|
||||
}
|
||||
});
|
||||
this.popOutButton.addEventListener("mouseleave", () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.background = "rgba(0, 0, 0, 0.4)";
|
||||
this.popOutButton.style.color = "rgba(255, 255, 255, 0.7)";
|
||||
}
|
||||
});
|
||||
this.glassDiv.appendChild(this.popOutButton);
|
||||
this.glassDiv.addEventListener("mouseenter", () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.opacity = "1";
|
||||
}
|
||||
});
|
||||
this.glassDiv.addEventListener("mouseleave", () => {
|
||||
if (this.popOutButton) {
|
||||
this.popOutButton.style.opacity = "0";
|
||||
}
|
||||
});
|
||||
document.body.appendChild(this.glassDiv);
|
||||
if (this.config.debugMode) {
|
||||
this.createDebugCanvas();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,293 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🔍 MagnifyGlass Viewer</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.header-title svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: #93c5fd;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 60px 20px 20px;
|
||||
}
|
||||
|
||||
#viewer-canvas {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 80px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
border: 2px solid rgba(99, 102, 241, 0.3);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.disconnected-message {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disconnected-message.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.disconnected-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.disconnected-title {
|
||||
font-size: 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.disconnected-hint {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.connecting .status-dot {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
background: #fbbf24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="connecting">
|
||||
<header class="header">
|
||||
<div class="header-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<span>MagnifyGlass Viewer</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">Connecting...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas id="viewer-canvas" width="300" height="300"></canvas>
|
||||
<div class="disconnected-message">
|
||||
<span class="disconnected-icon">🔍</span>
|
||||
<p class="disconnected-title">Not Connected</p>
|
||||
<p class="disconnected-hint">Activate MagnifyGlass in ComfyUI to start viewing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CHANNEL_NAME = 'comfyui-magnifyglass-popout';
|
||||
|
||||
// DOM Elements
|
||||
const canvas = document.getElementById('viewer-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
const statusText = document.querySelector('.status-text');
|
||||
const disconnectedMessage = document.querySelector('.disconnected-message');
|
||||
const body = document.body;
|
||||
|
||||
// State
|
||||
let isConnected = false;
|
||||
let lastFrameTime = 0;
|
||||
let frameImage = new Image();
|
||||
let connectionCheckInterval = null;
|
||||
|
||||
// Initialize BroadcastChannel
|
||||
const channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
|
||||
channel.onmessage = (event) => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'frame':
|
||||
handleFrame(message.data);
|
||||
break;
|
||||
case 'config':
|
||||
handleConfig(message.data);
|
||||
break;
|
||||
case 'ping':
|
||||
// Respond with pong
|
||||
channel.postMessage({ type: 'pong', timestamp: Date.now() });
|
||||
markConnected();
|
||||
break;
|
||||
case 'close':
|
||||
handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function handleFrame(dataUrl) {
|
||||
if (!dataUrl) return;
|
||||
|
||||
frameImage.onload = () => {
|
||||
// Resize canvas to match image dimensions
|
||||
if (canvas.width !== frameImage.width || canvas.height !== frameImage.height) {
|
||||
canvas.width = frameImage.width;
|
||||
canvas.height = frameImage.height;
|
||||
}
|
||||
|
||||
// Draw the frame
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(frameImage, 0, 0);
|
||||
};
|
||||
|
||||
frameImage.src = dataUrl;
|
||||
lastFrameTime = Date.now();
|
||||
markConnected();
|
||||
}
|
||||
|
||||
function handleConfig(config) {
|
||||
if (!config) return;
|
||||
|
||||
// Apply styling based on config
|
||||
if (config.borderColor) {
|
||||
canvas.style.borderColor = config.borderColor;
|
||||
}
|
||||
if (config.borderWidth) {
|
||||
canvas.style.borderWidth = `${config.borderWidth}px`;
|
||||
}
|
||||
if (config.glassShape === 'Circle') {
|
||||
canvas.style.borderRadius = '50%';
|
||||
} else if (config.glassShape === 'Rounded Square') {
|
||||
canvas.style.borderRadius = '20%';
|
||||
} else {
|
||||
canvas.style.borderRadius = '8px';
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
markDisconnected();
|
||||
// Optionally close this tab
|
||||
// window.close();
|
||||
}
|
||||
|
||||
function markConnected() {
|
||||
if (!isConnected) {
|
||||
isConnected = true;
|
||||
body.classList.remove('connecting');
|
||||
statusDot.classList.add('connected');
|
||||
statusText.textContent = 'Connected';
|
||||
canvas.style.display = 'block';
|
||||
disconnectedMessage.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
function markDisconnected() {
|
||||
isConnected = false;
|
||||
body.classList.add('connecting');
|
||||
statusDot.classList.remove('connected');
|
||||
statusText.textContent = 'Disconnected';
|
||||
canvas.style.display = 'none';
|
||||
disconnectedMessage.classList.add('show');
|
||||
}
|
||||
|
||||
// Check connection status periodically
|
||||
connectionCheckInterval = setInterval(() => {
|
||||
if (isConnected && Date.now() - lastFrameTime > 3000) {
|
||||
markDisconnected();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Initial ping to source
|
||||
channel.postMessage({ type: 'pong', timestamp: Date.now() });
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
channel.postMessage({ type: 'close' });
|
||||
channel.close();
|
||||
clearInterval(connectionCheckInterval);
|
||||
});
|
||||
|
||||
// Handle Escape key to close
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
channel.postMessage({ type: 'close' });
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -40,8 +40,10 @@ const STORAGE_KEYS = {
|
||||
OFFSET_X: "comfyui_magnify_offset_x",
|
||||
OFFSET_Y: "comfyui_magnify_offset_y"
|
||||
};
|
||||
const BROADCAST_CHANNEL_NAME = "comfyui-magnifyglass-popout";
|
||||
export {
|
||||
ACTIVATION_KEYS,
|
||||
BROADCAST_CHANNEL_NAME,
|
||||
DEFAULT_BORDER_COLOR,
|
||||
DEFAULT_BORDER_WIDTH,
|
||||
DEFAULT_GLASS_SIZE,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 4;\nexport const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap)\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 300;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 300;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;"}
|
||||
{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 4;\nexport const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap)\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 300;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 300;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n\n// BroadcastChannel for pop-out tab communication\nexport const BROADCAST_CHANNEL_NAME = 'comfyui-magnifyglass-popout';\n\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;AAGO,MAAM,yBAAyB;"}
|
||||
@@ -57,6 +57,12 @@ const Icons = {
|
||||
// Magnet icon - persist/sticky mode
|
||||
magnet: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 15-4-4 6.75-6.77a7.79 7.79 0 0 1 11 11L13 22l-4-4 6.39-6.36a2.2 2.2 0 0 0-3.1-3.13l-6.29 6.49Z"/>
|
||||
</svg>`,
|
||||
// External link icon - pop out to new tab
|
||||
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>`
|
||||
};
|
||||
export {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user