482 lines
19 KiB
JavaScript
482 lines
19 KiB
JavaScript
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);
|