Files
ty0x2333-ComfyUI-Dev-Utils/web/logConsole.js
T

313 lines
10 KiB
JavaScript

import {app} from "../../../scripts/app.js";
import {api} from "../../../scripts/api.js";
import {$el} from "../../../scripts/ui.js";
function addScript(src) {
$el("script", {
parent: document.head,
src: src
});
}
function addCss(src) {
$el("link", {
parent: document.head,
rel: "stylesheet",
type: "text/css",
href: src
});
}
const getValue = (key, defaultValue) => {
const storageKey = localStorage.getItem("TyDev-Utils.LogConsole." + key);
if (storageKey && !isNaN(+storageKey)) {
return storageKey;
}
return defaultValue;
};
const setValue = (key, value) => {
localStorage.setItem("TyDev-Utils.LogConsole." + key, value);
};
let logConsoleEnabled = true;
// https://stackoverflow.com/a/8809472
function generateUUID() { // Public Domain/MIT
var d = new Date().getTime();//Timestamp
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16;//random number between 0 and 16
if (d > 0) {//Use timestamp until depleted
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {//Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
let consoleId = generateUUID()
app.registerExtension({
name: "TyDev-Utils.LogConsole",
eventSource: null,
terminal: null,
async setup() {
const showButton = $el("button.comfy-settings-btn", {
style: {
right: "16px",
cursor: "pointer"
},
}, [
$el("img", {
src: new URL("images/terminal@2x.png", import.meta.url).toString(),
style: {
marginTop: "2px",
width: "14px",
height: "14px"
}
})
]);
document.querySelector(".comfy-settings-btn").after(showButton);
addScript(new URL("vendor/xterm.min.js", import.meta.url).toString());
addCss(new URL("vendor/xterm.min.css", import.meta.url).toString());
addScript(new URL("vendor/xterm-addon-fit.min.js", import.meta.url).toString());
addScript(new URL("vendor/interact.min.js", import.meta.url).toString());
addCss(new URL("logConsole.css", import.meta.url).toString());
const consoleElem = $el('div');
consoleElem.id = 'tydev-utils-log-console';
const expandButton = $el("button.expand-button", [
$el("div.arrow.arrow-down", [$el("i.arrow-icon")])
]);
const closeButton = $el("button.comfy-close-menu-btn", {textContent: 'x'});
const clearButton = $el("button", {
textContent: '🗑️',
style: {
fontSize: '14px'
},
onclick: () => {
this.terminal?.clear();
}
});
const consoleMenuContainer = $el("div.tydev-utils-log-console-menu-container", [
clearButton,
closeButton
])
const stateElem = $el("strong");
stateElem.id = "tydev-utils-log-console-state";
const consoleIdElem = $el("div", {
textContent: `ID: ${consoleId}`,
style: {
fontSize: '12px',
marginLeft: '8px',
lineHeight: '20px',
color: 'var(--descrip-text)'
}
});
const consoleStateElem = $el("div", {style: {display: 'flex', flexDirection: 'row'}}, [
stateElem,
consoleIdElem
]);
const headerElem = $el("div.tydev-utils-log-console-control", [
consoleStateElem,
expandButton,
consoleMenuContainer
]);
const containerElem = $el("div.tydev-utils-log-console-container", [
headerElem,
consoleElem
]);
document.body.append(containerElem);
const defaultHeight = 240;
const controlHeight = 20;
expandButton.onclick = () => {
const open = !expandButton.firstChild.className.includes("arrow-down");
if (open) {
expandButton.firstChild.className = "arrow arrow-down"
} else {
expandButton.firstChild.className = "arrow arrow-up"
}
containerElem.style.height = `${open ? defaultHeight : controlHeight}px`
containerElem.setAttribute('data-y', open ? 0 : defaultHeight - controlHeight);
};
interact(containerElem)
.resizable({
// resize from all edges and corners
edges: {left: false, right: false, bottom: false, top: true},
listeners: {
move(event) {
var target = event.target
// var x = (parseFloat(target.getAttribute('data-x')) || 0)
var y = parseFloat(target.getAttribute('data-y')) || 0
// update the element's style
// target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
// x += event.deltaRect.left
y += event.deltaRect.top
// target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
// target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
event.preventDefault();
// target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
}
},
modifiers: [
// keep the edges inside the parent
interact.modifiers.restrictEdges({
outer: 'parent'
}),
// minimum size
interact.modifiers.restrictSize({
min: {height: controlHeight}
})
],
inertia: true
})
const setConsoleVisible = (visible) => {
setValue('Visible', visible ? '1' : '0');
containerElem.hidden = !visible || !logConsoleEnabled;
showButton.hidden = visible || !logConsoleEnabled;
}
this.setupTerminal(containerElem, consoleElem);
showButton.onclick = () => {
setConsoleVisible(!(getValue('Visible', '1') === '1'));
}
closeButton.onclick = () => {
setConsoleVisible(false);
}
api.addEventListener("reconnected", () => {
if (this.eventSource) {
this.startSSE();
}
});
const onEnabledChange = (value) => {
logConsoleEnabled = value;
if (value) {
this.startSSE();
} else {
this.stopSSE();
}
const visible = getValue('Visible', '1') === '1';
setConsoleVisible(visible);
if (!value) {
api.fetchApi(`/ty-dev-utils/disable-log?console_id=${consoleId}&client_id=${api.clientId}`, {
method: "POST"
});
}
}
logConsoleEnabled = app.ui.settings.addSetting({
id: "TyDev-Utils.LogConsole.Enabled",
name: "TyDev LogConsole Enabled",
type: "boolean",
defaultValue: true,
onChange: onEnabledChange
});
},
setupTerminal(containerElem, consoleElem) {
if (this.terminal) {
return;
}
this.terminal = new Terminal({
convertEol: true,
});
const terminal = this.terminal;
this.terminal.attachCustomKeyEventHandler((e) => {
if (e.ctrlKey && e.keyCode === 76) {
// Ctrl + L
terminal.clear();
return false;
}
});
const fitAddon = new FitAddon.FitAddon();
this.terminal.loadAddon(fitAddon);
this.terminal.open(consoleElem);
fitAddon.fit();
const resizeObserver = new ResizeObserver(function (entries) {
try {
fitAddon && fitAddon.fit();
} catch (err) {
console.log(err);
}
});
resizeObserver.observe(containerElem);
},
startSSE() {
if ([EventSource.OPEN, EventSource.CONNECTING].includes(this.eventSource?.readyState)) {
return
}
const logSSEUrl = api.apiURL(`/ty-dev-utils/log?console_id=${consoleId}&client_id=${api.clientId}`);
this.eventSource = new EventSource(logSSEUrl);
this.eventSource.onopen = () => {
// console.log('EventSource connected')
this.setSSEState(this.eventSource.readyState);
};
this.eventSource.onerror = (error) => {
// console.error('EventSource failed', error)
this.eventSource.close();
this.setSSEState(this.eventSource.readyState);
};
const messageHandler = (event) => {
this.terminal?.write(event.data);
// console.log(event.data);
}
this.eventSource.addEventListener("message", messageHandler);
},
stopSSE() {
this.eventSource?.close();
this.setSSEState(EventSource.CLOSED);
this.eventSource = null;
},
setSSEState(state) {
const stateElem = document.getElementById("tydev-utils-log-console-state");
if (stateElem) {
let stateTextColor;
let stateText;
if (state === EventSource.OPEN) {
stateTextColor = "green";
stateText = "CONNECTED";
} else if (state === EventSource.CONNECTING) {
stateTextColor = "gold";
stateText = "CONNECTING";
} else if (state === EventSource.CLOSED) {
stateTextColor = 'red';
stateText = "CLOSED";
}
stateElem.style.color = stateTextColor;
stateElem.innerText = stateText;
}
}
});