Files
VrchStudio-comfyui-web-viewer/web/comfyui/websocket_nodes.js
T

482 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from '../../../scripts/widgets.js';
import {
hideWidget,
showWidget,
buildUrl,
delayInit,
createOpenWebViewerButton,
setupWidgetCallback,
} from "./node_utils.js";
// =====================================================================
// Helper: Create Server Status Indicator Element
// =====================================================================
/**
* Create a non‑clickable circular indicator element for server status.
* @returns {HTMLElement}
*/
function createServerStatusIndicatorElement() {
const indicator = document.createElement("div");
indicator.textContent = "Run to Start Server"; // Initial text
indicator.classList.add("vrch-server-indicator", "off"); // Start as off
indicator.style.pointerEvents = "none";
return indicator;
}
// =====================================================================
// Extension: vrch.ImageWebSocketWebViewer
// =====================================================================
app.registerExtension({
name: "vrch.ImageWebSocketWebViewer",
async nodeCreated(node) {
if (node.comfyClass === "VrchImageWebSocketWebViewerNode") {
// Find existing widgets
const serverWidget = node.widgets.find(w => w.name === "server");
const channelWidget = node.widgets.find(w => w.name === "channel");
const numberOfImagesWidget = node.widgets.find(w => w.name === "number_of_images");
const imageDisplayDurationWidget = node.widgets.find(w => w.name === "image_display_duration");
const fadeAnimDurationWidget = node.widgets.find(w => w.name === "fade_anim_duration");
const blendModeWidget = node.widgets.find(w => w.name === "blend_mode");
const loopPlaybackWidget = node.widgets.find(w => w.name === "loop_playback");
const updateOnEndWidget = node.widgets.find(w => w.name === "update_on_end");
const backgroundColorWidget = node.widgets.find(w => w.name === "background_colour_hex");
const widthWidget = node.widgets.find(w => w.name === "window_width");
const heightWidget = node.widgets.find(w => w.name === "window_height");
const extraParamsWidget = node.widgets.find(w => w.name === "extra_params");
const urlWidget = node.widgets.find(w => w.name === "url");
const showUrlWidget = node.widgets.find(w => w.name === "show_url");
const devModeWidget = node.widgets.find(w => w.name === "dev_mode");
function updateUrl() {
if (urlWidget) {
urlWidget.value = buildUrl({
serverWidget: serverWidget,
extraParamsWidget: extraParamsWidget,
mode: "image-websocket",
protocol: "websocket",
devMode: devModeWidget,
additionalParams: {
channel: channelWidget,
numberOfImages: numberOfImagesWidget,
imageDisplayDuration: imageDisplayDurationWidget,
fadeAnimDuration: fadeAnimDurationWidget,
mixBlendMode: blendModeWidget,
enableLoop: loopPlaybackWidget,
enableUpdateOnEnd: updateOnEndWidget,
bgColourPicker: backgroundColorWidget,
}
});
}
}
// Use setupWidgetCallback() with a custom log prefix if desired
setupWidgetCallback(
node,
updateUrl,
urlWidget,
showUrlWidget,
[
serverWidget,
channelWidget,
extraParamsWidget,
numberOfImagesWidget,
imageDisplayDurationWidget,
fadeAnimDurationWidget,
blendModeWidget,
loopPlaybackWidget,
updateOnEndWidget,
backgroundColorWidget,
devModeWidget,
],
"VrchImageWebSocketWebViewerNode"
);
hideWidget(node, urlWidget);
createOpenWebViewerButton(node, urlWidget, widthWidget, heightWidget);
delayInit(node, showUrlWidget, urlWidget, updateUrl);
}
}
});
// =====================================================================
// Extension: vrch.ImageWebSocketSimpleWebViewer
// =====================================================================
app.registerExtension({
name: "vrch.ImageWebSocketSimpleWebViewer",
async nodeCreated(node) {
if (node.comfyClass === "VrchImageWebSocketSimpleWebViewerNode") {
// Find existing widgets
const serverWidget = node.widgets.find(w => w.name === "server");
const channelWidget = node.widgets.find(w => w.name === "channel");
const numberOfImagesWidget = node.widgets.find(w => w.name === "number_of_images");
const imageDisplayDurationWidget = node.widgets.find(w => w.name === "image_display_duration");
const fadeAnimDurationWidget = node.widgets.find(w => w.name === "fade_anim_duration");
const widthWidget = node.widgets.find(w => w.name === "window_width");
const heightWidget = node.widgets.find(w => w.name === "window_height");
const extraParamsWidget = node.widgets.find(w => w.name === "extra_params");
const urlWidget = node.widgets.find(w => w.name === "url");
const showUrlWidget = node.widgets.find(w => w.name === "show_url");
const devModeWidget = node.widgets.find(w => w.name === "dev_mode");
function updateUrl() {
if (urlWidget) {
urlWidget.value = buildUrl({
serverWidget: serverWidget,
extraParamsWidget: extraParamsWidget,
mode: "image-websocket",
protocol: "websocket",
devMode: devModeWidget,
additionalParams: {
channel: channelWidget,
numberOfImages: numberOfImagesWidget,
imageDisplayDuration: imageDisplayDurationWidget,
fadeAnimDuration: fadeAnimDurationWidget,
}
});
}
}
// Use setupWidgetCallback() with a custom log prefix if desired
setupWidgetCallback(
node,
updateUrl,
urlWidget,
showUrlWidget,
[
serverWidget,
channelWidget,
numberOfImagesWidget,
imageDisplayDurationWidget,
fadeAnimDurationWidget,
extraParamsWidget,
devModeWidget,
],
"VrchImageWebSocketSimpleWebViewerNode"
);
hideWidget(node, urlWidget);
createOpenWebViewerButton(node, urlWidget, widthWidget, heightWidget);
delayInit(node, showUrlWidget, urlWidget, updateUrl);
}
}
});
// =====================================================================
// Extension: vrch.WebSocketServer
// =====================================================================
app.registerExtension({
name: "vrch.WebSocketServer",
async nodeCreated(node) {
if (node.comfyClass === "VrchWebSocketServerNode") {
try {
// Find widgets
const debugWidget = node.widgets.find(w => w.name === "debug");
const serverWidget = node.widgets.find(w => w.name === "server");
const portWidget = node.widgets.find(w => w.name === "port");
// Create a container for the indicator
const container = document.createElement("div");
container.classList.add("vrch-server-indicator-container");
node.addDOMWidget("indicator_container", "Status", container);
// Initialize the indicator
const statusIndicator = createServerStatusIndicatorElement();
container.appendChild(statusIndicator);
// Create Full Address container with copy feature
const addrContainer = document.createElement("div");
addrContainer.classList.add("vrch-server-address-container");
const addrText = document.createElement("span");
addrText.classList.add("vrch-server-address");
addrText.textContent = "-";
addrContainer.appendChild(addrText);
node.addDOMWidget("full_address_container", "Address", addrContainer);
function computeAddress() {
const host = serverWidget?.value ?? "";
const port = portWidget?.value ?? "";
return `${host}:${port}`;
}
function updateAddress(fromExecuted) {
// Compute locally; onExecuted will override with server_full if provided
addrText.textContent = computeAddress();
}
async function copyToClipboard(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
// Show a checkmark via CSS without shifting layout
addrText.classList.add("copied");
setTimeout(() => { addrText.classList.remove("copied"); }, 1200);
} catch (e) {
console.warn("[VrchWebSocketServerNode] Copy failed", e);
}
}
addrText.addEventListener("click", () => {
copyToClipboard(addrText.textContent || "");
});
// Hook widget callbacks to refresh address
function hookWidget(widget) {
if (!widget) return;
const orig = widget.callback;
widget.callback = function() {
try { orig?.apply(this, arguments); } catch(e) { /* ignore */ }
try { updateAddress(false); } catch(e) { /* ignore */ }
};
}
hookWidget(serverWidget);
hookWidget(portWidget);
// initial
updateAddress(false);
// Update indicator based on execution results
const onExecuted = node.onExecuted;
node.onExecuted = function(message) {
onExecuted?.apply(this, arguments);
// Get server status and debug status from message
const isRunning = message?.server_status?.[0];
const isDebugEnabled = message?.debug_status?.[0];
if (typeof isRunning !== 'undefined') {
statusIndicator.textContent = isRunning ? "Running" : "Stopped";
statusIndicator.classList.toggle("on", !!isRunning);
statusIndicator.classList.toggle("off", !isRunning);
// Log status update if debug is enabled
if (isDebugEnabled) {
console.log(`[VrchWebSocketServerNode] Server status update: ${isRunning ? 'Running' : 'Stopped'}`);
}
}
// Update full address if provided
const serverFull = message?.server_full?.[0];
if (serverFull) {
addrText.textContent = serverFull;
} else {
updateAddress(true);
}
// If there's an error message from path validation, it would be handled by ComfyUI's error system
// We can add extra debug logging here if needed
if (isDebugEnabled && typeof isRunning === 'undefined') {
console.log("[VrchWebSocketServerNode] Executed but no status received:", message);
}
};
} catch (e) {
console.error("[vrch.WebSocketServer] UI init failed:", e);
}
}
}
});
// =====================================================================
// Extension: vrch.ImageWebSocketFilterSettings (add Reset Filters button)
// =====================================================================
app.registerExtension({
name: "vrch.ImageWebSocketFilterSettings",
async nodeCreated(node) {
if (node.comfyClass === "VrchImageWebSocketFilterSettingsNode") {
// New order (grayscale removed): opacity, brightness, contrast, saturate, hue_rotate, invert, sepia, blur
const widgetNames = [
"opacity",
"brightness",
"contrast",
"saturate",
"hue_rotate",
"invert",
"sepia",
"blur"
];
const defaults = {
opacity: 1.0,
brightness: 1.0,
contrast: 1.0,
saturate: 1.0,
hue_rotate: 0,
invert: 0.0,
sepia: 0.0,
blur: 0,
};
// Create button container
const container = document.createElement("div");
// Use a smaller dedicated container so it isn't as tall as the big viewer buttons
container.classList.add("vrch-filter-reset-container");
const btn = document.createElement("button");
btn.textContent = "Reset Filters";
btn.classList.add("vrch-filter-reset-btn");
container.appendChild(btn);
node.addDOMWidget("filter_reset_btn", "Reset", container);
function setWidgetValue(widget, value) {
if (!widget) return;
widget.value = value;
if (widget.callback) {
try { widget.callback(widget.value, node, widget); } catch(e) { /* ignore */ }
}
}
btn.addEventListener("click", () => {
widgetNames.forEach(name => {
const w = node.widgets.find(w => w.name === name);
if (w) setWidgetValue(w, defaults[name]);
});
// Force UI refresh
app.graph.setDirtyCanvas(true, true);
console.log("[VrchImageWebSocketFilterSettingsNode] Filters reset to defaults", defaults);
});
}
}
});
// Add custom styles for the button and indicator
const style = document.createElement("style");
style.textContent = `
/* Button container style */
.vrch-button-container {
width: 100%;
box-sizing: border-box;
display: flex;
height: 48px !important; /* Ensure the container has a fixed height */
align-items: center; /* Center vertically */
justify-content: center; /* Center horizontally */
}
.vrch-big-button {
background-color: #4CAF50;
color: white;
font-size: 16px;
font-weight: bold;
border: none;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: background-color 0.3s, transform 0.2s;
width: 100%;
height: 100%;
}
.vrch-big-button:hover {
background-color: #45a049;
transform: scale(1.01);
}
.vrch-big-button:active {
background-color: #3e8e41;
transform: scale(1);
}
/* Styles for Server Status Indicator */
.vrch-server-indicator-container {
display: flex;
justify-content: center;
align-items: center;
padding: 2px; /* tighter */
margin: 0; /* remove extra gap */
}
.vrch-server-indicator {
width: 200px;
height: 32px;
border-radius: 16px; /* Pill shape */
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
user-select: none;
border: 1px solid #555; /* Add border */
}
.vrch-server-indicator.off {
background-color: #ccc;
color: #333;
}
.vrch-server-indicator.on {
background-color: #4CAF50; /* Green */
color: #fff;
}
.vrch-filter-reset-container {
width: 100%;
box-sizing: border-box;
display: flex;
height: 32px; /* smaller than big buttons */
align-items: center;
justify-content: center;
}
.vrch-filter-reset-btn {
background-color: #4CAF50;
color: #fff;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
width: 90%;
height: 32px;
line-height: 1;
transition: background-color 0.25s, transform 0.2s;
padding: 4px 8px;
}
.vrch-filter-reset-btn:hover { background-color: #45a049; transform: scale(1.01); }
.vrch-filter-reset-btn:active { background-color: #3e8e41; transform: scale(1); }
/* Server full address (centered, clickable, checkmark overlay) */
.vrch-server-address-container {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 0; /* tighter */
margin-top: -4px; /* pull closer to status */
width: 100%;
box-sizing: border-box;
}
.vrch-server-address {
padding: 6px 14px;
background: #222;
border: 1px solid #555;
border-radius: 6px;
font-family: monospace;
font-size: 15px;
position: relative;
cursor: pointer;
}
.vrch-server-address::after {
content: "copied"; /* small indicator */
position: absolute;
right: -28px; /* outside top-right corner */
top: -8px;
color: #7CFC00;
font-weight: 700;
font-size: 11px; /* small text */
letter-spacing: 0.3px;
opacity: 0;
transform: scale(0.96);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
}
.vrch-server-address.copied::after {
opacity: 1;
transform: scale(1.0);
}
`;
document.head.appendChild(style);